@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/CHANGELOG.md +59 -0
- package/dist/index.d.mts +38 -214
- package/dist/index.mjs +1831 -1618
- package/package.json +18 -26
- package/dist/compiler-Ck-td5Ds.mjs +0 -48369
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
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$
|
|
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$
|
|
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/
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 ===
|
|
275
|
+
return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator;
|
|
253
276
|
}
|
|
254
277
|
//#endregion
|
|
255
|
-
//#region src/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
309
|
+
major,
|
|
310
|
+
minor
|
|
341
311
|
};
|
|
342
312
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
return true;
|
|
326
|
+
function isUnknownArray(value) {
|
|
327
|
+
return Array.isArray(value);
|
|
437
328
|
}
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
451
|
-
return
|
|
332
|
+
function isWalkFunction(value) {
|
|
333
|
+
return typeof value === "function";
|
|
452
334
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
477
|
-
return
|
|
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
|
|
480
|
-
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
["
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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/
|
|
687
|
-
const { types: types$
|
|
508
|
+
//#region src/parsers/ts-parser.ts
|
|
509
|
+
const { types: types$19, parseAsync } = babel;
|
|
510
|
+
const require = createRequire(import.meta.url);
|
|
688
511
|
/**
|
|
689
|
-
*
|
|
690
|
-
* @param
|
|
691
|
-
* @
|
|
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
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
758
|
-
return
|
|
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
|
|
761
|
-
|
|
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/
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return
|
|
843
|
-
|
|
844
|
-
|
|
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/
|
|
862
|
-
const
|
|
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
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
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
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
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
|
|
897
|
-
return
|
|
627
|
+
function disableTypeCheckingInBabelAst(ast) {
|
|
628
|
+
return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments));
|
|
898
629
|
}
|
|
899
|
-
function
|
|
900
|
-
|
|
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
|
|
903
|
-
|
|
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
|
|
906
|
-
|
|
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
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
*
|
|
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
|
|
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/
|
|
974
|
-
const { types:
|
|
794
|
+
//#region src/babel/clone.ts
|
|
795
|
+
const { types: types$18 } = babel;
|
|
975
796
|
/**
|
|
976
|
-
*
|
|
977
|
-
* It uses an `if` statement to do so
|
|
797
|
+
* Helper for `types.cloneNode(node, deep: true, withoutLocations: false);`
|
|
978
798
|
*/
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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/
|
|
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
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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/
|
|
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/
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
|
1277
|
-
const
|
|
1278
|
-
if (
|
|
1279
|
-
return
|
|
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
|
|
1282
|
-
|
|
1283
|
-
|
|
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/
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
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/
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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/
|
|
1431
|
+
//#region src/mutators/registry.ts
|
|
1333
1432
|
/**
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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/
|
|
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$
|
|
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.${
|
|
1369
|
-
if (ns.${
|
|
1370
|
-
ns.${
|
|
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.${
|
|
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.${
|
|
1386
|
-
c = cov.perTest[ns.${
|
|
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.${
|
|
1400
|
-
if (ns.${
|
|
1401
|
-
throw new Error('Stryker: Hit count limit reached (' + ns.${
|
|
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$
|
|
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
|
-
|
|
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
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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$
|
|
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
|
-
*
|
|
1496
|
-
* @param
|
|
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
|
|
1500
|
-
|
|
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
|
-
*
|
|
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
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
|
|
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
|
-
*
|
|
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
|
|
1516
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
|
1540
|
-
return
|
|
1685
|
+
function nameIfAnonymous(path) {
|
|
1686
|
+
return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
|
|
1541
1687
|
}
|
|
1542
|
-
function
|
|
1543
|
-
return
|
|
1688
|
+
function isMemberOrCallOrNonNullExpression(path) {
|
|
1689
|
+
return isCallExpression(path) || isMemberOrNonNullExpression(path);
|
|
1544
1690
|
}
|
|
1545
|
-
function
|
|
1546
|
-
return
|
|
1691
|
+
function isMemberOrNonNullExpression(path) {
|
|
1692
|
+
return isMemberExpression(path) || path.isTSNonNullExpression();
|
|
1547
1693
|
}
|
|
1548
|
-
function
|
|
1549
|
-
return
|
|
1694
|
+
function isMemberExpression(path) {
|
|
1695
|
+
return path.isMemberExpression() || path.isOptionalMemberExpression();
|
|
1550
1696
|
}
|
|
1551
|
-
function
|
|
1552
|
-
return
|
|
1697
|
+
function isCallExpression(path) {
|
|
1698
|
+
return path.isCallExpression() || path.isOptionalCallExpression();
|
|
1553
1699
|
}
|
|
1554
|
-
function
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
1624
|
-
const
|
|
1625
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
1659
|
-
if (
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
1666
|
-
|
|
1667
|
-
isExpression: false
|
|
1959
|
+
ignorers,
|
|
1960
|
+
activeIgnored: void 0
|
|
1668
1961
|
};
|
|
1669
1962
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
|
1704
|
-
if (
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
rawContent: text
|
|
2099
|
+
parent: ancestors[0],
|
|
2100
|
+
grandParent: ancestors[1],
|
|
2101
|
+
ancestors
|
|
1733
2102
|
};
|
|
1734
2103
|
}
|
|
1735
|
-
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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/
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
case "
|
|
1781
|
-
case "
|
|
1782
|
-
case "
|
|
1783
|
-
case "
|
|
1784
|
-
case "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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 {
|
|
2327
|
+
export { angularIgnorer, disableTypeChecks, frameworkPluginsFileUrl, instrument, strykerPlugins };
|