@systemfsoftware/stryker-js-instrumenter 0.1.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/LICENSE +212 -0
- package/README.md +25 -0
- package/dist/compiler-Ck-td5Ds.mjs +48369 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.mjs +2114 -0
- package/package.json +80 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2114 @@
|
|
|
1
|
+
import { PluginKind, commonTokens, declareClassPlugin, tokens } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { INSTRUMENTER_CONSTANTS as ID } from "@systemfsoftware/stryker-js-plugin-api/core";
|
|
4
|
+
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
|
+
import { createRequire } from "module";
|
|
12
|
+
//#region src/instrumenter-tokens.ts
|
|
13
|
+
const instrumenterTokens = Object.freeze({
|
|
14
|
+
createParser: "instrumenterCreateParser",
|
|
15
|
+
print: "instrumenterPrint",
|
|
16
|
+
transform: "instrumenterTransform"
|
|
17
|
+
});
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/parsers/parse-error.ts
|
|
20
|
+
var ParseError = class extends Error {
|
|
21
|
+
constructor(message, fileName, location) {
|
|
22
|
+
super(`Parse error in ${fileName} (${location.line}:${location.column}) ${message}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/parsers/html-parser.ts
|
|
27
|
+
const TSX_SCRIPT_TYPES = Object.freeze(["tsx", "text/tsx"]);
|
|
28
|
+
const TS_SCRIPT_TYPES = Object.freeze([
|
|
29
|
+
"ts",
|
|
30
|
+
"text/typescript",
|
|
31
|
+
"typescript"
|
|
32
|
+
]);
|
|
33
|
+
const JS_SCRIPT_TYPES = Object.freeze([
|
|
34
|
+
"js",
|
|
35
|
+
"text/javascript",
|
|
36
|
+
"javascript",
|
|
37
|
+
"module"
|
|
38
|
+
]);
|
|
39
|
+
async function parse$2(text, originFileName, context) {
|
|
40
|
+
return {
|
|
41
|
+
originFileName,
|
|
42
|
+
rawContent: text,
|
|
43
|
+
format: "html",
|
|
44
|
+
root: await ngHtmlParser(text, originFileName, context)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function ngHtmlParser(text, fileName, parserContext) {
|
|
48
|
+
const ngParser = await import("angular-html-parser");
|
|
49
|
+
const { rootNodes, errors } = ngParser.parse(text, {
|
|
50
|
+
canSelfClose: true,
|
|
51
|
+
allowHtmComponentClosingTags: true,
|
|
52
|
+
isTagNameCaseSensitive: true
|
|
53
|
+
});
|
|
54
|
+
if (errors.length !== 0) {
|
|
55
|
+
const firstError = errors[0];
|
|
56
|
+
if (firstError === void 0) throw new Error("HTML parser reported errors but first error is missing");
|
|
57
|
+
throw new ParseError(firstError.msg, fileName, toSourceLocation(firstError.span.start));
|
|
58
|
+
}
|
|
59
|
+
const scriptsAsPromised = [];
|
|
60
|
+
ngParser.visitAll(new class extends ngParser.RecursiveVisitor {
|
|
61
|
+
visitElement(el, context) {
|
|
62
|
+
const scriptFormat = getScriptType(el);
|
|
63
|
+
if (scriptFormat) scriptsAsPromised.push(parseScript(el, scriptFormat));
|
|
64
|
+
super.visitElement(el, context);
|
|
65
|
+
}
|
|
66
|
+
}(), rootNodes);
|
|
67
|
+
return { scripts: await Promise.all(scriptsAsPromised) };
|
|
68
|
+
async function parseScript(el, scriptFormat) {
|
|
69
|
+
const endSourceSpan = el.endSourceSpan;
|
|
70
|
+
if (endSourceSpan === null || endSourceSpan === void 0) throw new Error("HTML element without an end source span");
|
|
71
|
+
const content = text.substring(el.startSourceSpan.end.offset, endSourceSpan.start.offset);
|
|
72
|
+
const ast = await parserContext.parse(content, fileName, scriptFormat);
|
|
73
|
+
if (ast) {
|
|
74
|
+
const offset = el.startSourceSpan.end;
|
|
75
|
+
const rootStart = ast.root.start;
|
|
76
|
+
if (rootStart === null || rootStart === void 0) throw new Error("Babel File node without a start offset");
|
|
77
|
+
const rootEnd = ast.root.end;
|
|
78
|
+
if (rootEnd === null || rootEnd === void 0) throw new Error("Babel File node without an end offset");
|
|
79
|
+
ast.root.start = rootStart + offset.offset;
|
|
80
|
+
ast.root.end = rootEnd + offset.offset;
|
|
81
|
+
return {
|
|
82
|
+
...ast,
|
|
83
|
+
offset: {
|
|
84
|
+
column: offset.offset,
|
|
85
|
+
line: offset.line
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return ast;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function toSourceLocation({ line, col }) {
|
|
93
|
+
return {
|
|
94
|
+
line: line + 1,
|
|
95
|
+
column: col
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function getScriptType(element) {
|
|
99
|
+
if (element.name === "script") {
|
|
100
|
+
if (!element.attrs.some((attr) => attr.name === "src")) {
|
|
101
|
+
const type = element.attrs.find((attr) => attr.name === "type") ?? element.attrs.find((attr) => attr.name === "lang");
|
|
102
|
+
if (type) {
|
|
103
|
+
const typeToLower = type.value.toLowerCase();
|
|
104
|
+
if (TSX_SCRIPT_TYPES.includes(typeToLower)) return "tsx";
|
|
105
|
+
if (TS_SCRIPT_TYPES.includes(typeToLower)) return "ts";
|
|
106
|
+
if (JS_SCRIPT_TYPES.includes(typeToLower)) return "js";
|
|
107
|
+
} else return "js";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/parsers/js-parser.ts
|
|
113
|
+
const { types: types$15, parseAsync: parseAsync$1 } = babel;
|
|
114
|
+
function isParserPlugin(value) {
|
|
115
|
+
return typeof value === "string" || Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
function isParserPluginArray(value) {
|
|
118
|
+
return value.every(isParserPlugin);
|
|
119
|
+
}
|
|
120
|
+
function getParserPlugins(override) {
|
|
121
|
+
if (override === null || override === void 0) return defaultPlugins;
|
|
122
|
+
if (isParserPluginArray(override)) return override;
|
|
123
|
+
throw new Error("Invalid parser plugins: expected ParserPlugin[]");
|
|
124
|
+
}
|
|
125
|
+
const defaultPlugins = [
|
|
126
|
+
"doExpressions",
|
|
127
|
+
"objectRestSpread",
|
|
128
|
+
"classProperties",
|
|
129
|
+
"exportDefaultFrom",
|
|
130
|
+
"exportNamespaceFrom",
|
|
131
|
+
"asyncGenerators",
|
|
132
|
+
"functionBind",
|
|
133
|
+
"functionSent",
|
|
134
|
+
"dynamicImport",
|
|
135
|
+
"numericSeparator",
|
|
136
|
+
"importMeta",
|
|
137
|
+
"optionalCatchBinding",
|
|
138
|
+
"optionalChaining",
|
|
139
|
+
"classPrivateProperties",
|
|
140
|
+
["pipelineOperator", { proposal: "minimal" }],
|
|
141
|
+
"nullishCoalescingOperator",
|
|
142
|
+
"bigInt",
|
|
143
|
+
"throwExpressions",
|
|
144
|
+
"logicalAssignment",
|
|
145
|
+
"classPrivateMethods",
|
|
146
|
+
"v8intrinsic",
|
|
147
|
+
"partialApplication",
|
|
148
|
+
["decorators", { decoratorsBeforeExport: false }],
|
|
149
|
+
"jsx"
|
|
150
|
+
];
|
|
151
|
+
function createParser$1({ plugins: pluginsOverride }) {
|
|
152
|
+
return async function parse(text, fileName) {
|
|
153
|
+
const plugins = getParserPlugins(pluginsOverride);
|
|
154
|
+
const ast = await parseAsync$1(text, {
|
|
155
|
+
parserOpts: { plugins: [...plugins] },
|
|
156
|
+
filename: fileName,
|
|
157
|
+
sourceType: "module"
|
|
158
|
+
});
|
|
159
|
+
if (ast === null || ast === void 0) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
|
|
160
|
+
if (types$15.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
|
|
161
|
+
return {
|
|
162
|
+
originFileName: fileName,
|
|
163
|
+
rawContent: text,
|
|
164
|
+
format: "js",
|
|
165
|
+
root: ast
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/util/position-converter.ts
|
|
171
|
+
/**
|
|
172
|
+
* A class that can convert a string offset back to line / column.
|
|
173
|
+
* Grabbed from TypeScript code base
|
|
174
|
+
* @see https://github.com/microsoft/TypeScript/blob/aa9b6953441b53f8b14072c047f0519b611150c4/src/compiler/scanner.ts#L503
|
|
175
|
+
*/
|
|
176
|
+
var PositionConverter = class {
|
|
177
|
+
text;
|
|
178
|
+
_lineStarts;
|
|
179
|
+
get lineStarts() {
|
|
180
|
+
if (!this._lineStarts) this._lineStarts = this.computeLineStarts(this.text);
|
|
181
|
+
return this._lineStarts;
|
|
182
|
+
}
|
|
183
|
+
constructor(text) {
|
|
184
|
+
this.text = text;
|
|
185
|
+
}
|
|
186
|
+
positionFromOffset(offset) {
|
|
187
|
+
const lineNumber = this.computeLineOfPosition(offset);
|
|
188
|
+
const lineStart = this.lineStarts[lineNumber];
|
|
189
|
+
if (lineStart === void 0) throw new Error("Line start not found for computed line number");
|
|
190
|
+
return {
|
|
191
|
+
line: lineNumber,
|
|
192
|
+
column: offset - lineStart
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
computeLineOfPosition(offset) {
|
|
196
|
+
let lineNumber = binarySearch(this.lineStarts, offset);
|
|
197
|
+
if (lineNumber < 0) {
|
|
198
|
+
lineNumber = ~lineNumber - 1;
|
|
199
|
+
if (lineNumber === -1) throw new Error("position cannot precede the beginning of the file");
|
|
200
|
+
}
|
|
201
|
+
return lineNumber;
|
|
202
|
+
}
|
|
203
|
+
computeLineStarts(text) {
|
|
204
|
+
const result = [];
|
|
205
|
+
let pos = 0;
|
|
206
|
+
let lineStart = 0;
|
|
207
|
+
while (pos < text.length) {
|
|
208
|
+
const ch = text.charCodeAt(pos);
|
|
209
|
+
pos++;
|
|
210
|
+
switch (ch) {
|
|
211
|
+
case 13:
|
|
212
|
+
if (text.charCodeAt(pos) === 10) pos++;
|
|
213
|
+
result.push(lineStart);
|
|
214
|
+
lineStart = pos;
|
|
215
|
+
break;
|
|
216
|
+
case 10:
|
|
217
|
+
result.push(lineStart);
|
|
218
|
+
lineStart = pos;
|
|
219
|
+
break;
|
|
220
|
+
default: if (ch > 127 && isLineBreak(ch)) {
|
|
221
|
+
result.push(lineStart);
|
|
222
|
+
lineStart = pos;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
result.push(lineStart);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function binarySearch(array, value) {
|
|
231
|
+
if (!array.length) return -1;
|
|
232
|
+
let low = 0;
|
|
233
|
+
let high = array.length - 1;
|
|
234
|
+
while (low <= high) {
|
|
235
|
+
const middle = low + (high - low >> 1);
|
|
236
|
+
const midValue = array[middle];
|
|
237
|
+
if (midValue === void 0) throw new Error("Binary search middle value is missing");
|
|
238
|
+
switch (compare(midValue, value)) {
|
|
239
|
+
case -1:
|
|
240
|
+
low = middle + 1;
|
|
241
|
+
break;
|
|
242
|
+
case 0: return middle;
|
|
243
|
+
case 1: high = middle - 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return ~low;
|
|
247
|
+
}
|
|
248
|
+
function compare(a, b) {
|
|
249
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
250
|
+
}
|
|
251
|
+
function isLineBreak(ch) {
|
|
252
|
+
return ch === 10 || ch === 13 || ch === 8232 || ch === 8233;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/util/babel-generator.ts
|
|
256
|
+
/**
|
|
257
|
+
* `@babel/generator` is CommonJS. Under Node's own ESM interop a default import
|
|
258
|
+
* of it is the module's `exports` object, so the code generator sits behind
|
|
259
|
+
* `.default` — the shape upstream reaches for, because upstream ships one
|
|
260
|
+
* emitted file per source file. This package ships a bundle, where the default
|
|
261
|
+
* import is already the function and `.default` is `undefined`, which fails at
|
|
262
|
+
* the first mutant with `generator is not a function` rather than at build
|
|
263
|
+
* time. Resolving both shapes once keeps the printers and the mutant's
|
|
264
|
+
* replacement code identical under either layout.
|
|
265
|
+
*/
|
|
266
|
+
const generate = typeof generator === "function" ? generator : generator.default;
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/mutant.ts
|
|
269
|
+
const { traverse: traverse$2 } = babel;
|
|
270
|
+
var Mutant$1 = class {
|
|
271
|
+
id;
|
|
272
|
+
fileName;
|
|
273
|
+
original;
|
|
274
|
+
offset;
|
|
275
|
+
replacementCode;
|
|
276
|
+
replacement;
|
|
277
|
+
mutatorName;
|
|
278
|
+
ignoreReason;
|
|
279
|
+
constructor(id, fileName, original, specs, offset = {
|
|
280
|
+
column: 0,
|
|
281
|
+
line: 0
|
|
282
|
+
}) {
|
|
283
|
+
this.id = id;
|
|
284
|
+
this.fileName = fileName;
|
|
285
|
+
this.original = original;
|
|
286
|
+
this.offset = offset;
|
|
287
|
+
this.replacement = specs.replacement;
|
|
288
|
+
this.mutatorName = specs.mutatorName;
|
|
289
|
+
this.ignoreReason = specs.ignoreReason;
|
|
290
|
+
this.replacementCode = generate(this.replacement).code;
|
|
291
|
+
}
|
|
292
|
+
toApiMutant() {
|
|
293
|
+
const loc = this.original.loc;
|
|
294
|
+
if (loc === void 0 || loc === null) throw new Error("Babel node without a source location");
|
|
295
|
+
return {
|
|
296
|
+
fileName: this.fileName,
|
|
297
|
+
id: this.id,
|
|
298
|
+
location: toApiLocation(loc, this.offset),
|
|
299
|
+
mutatorName: this.mutatorName,
|
|
300
|
+
replacement: this.replacementCode,
|
|
301
|
+
...this.ignoreReason === void 0 ? {} : { statusReason: this.ignoreReason },
|
|
302
|
+
...this.ignoreReason === void 0 ? {} : { status: "Ignored" }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Applies the mutant in (a copy of) the AST, without changing provided AST.
|
|
307
|
+
* Can the tree itself (in which case the replacement is returned),
|
|
308
|
+
* or can be nested in the given tree.
|
|
309
|
+
*
|
|
310
|
+
* Returns a plain node rather than the argument's own type: whether the
|
|
311
|
+
* replacement fits a given position is the placer's claim, and the placer
|
|
312
|
+
* checks it with a Babel predicate. A generic return would have to assert it
|
|
313
|
+
* here, where nothing can check it.
|
|
314
|
+
* @param originalTree The original node, which will be treated as readonly
|
|
315
|
+
*/
|
|
316
|
+
applied(originalTree) {
|
|
317
|
+
if (originalTree === this.original) return this.replacement;
|
|
318
|
+
else {
|
|
319
|
+
const mutatedAst = deepCloneNode(originalTree);
|
|
320
|
+
let applied = false;
|
|
321
|
+
const { original, replacement } = this;
|
|
322
|
+
traverse$2(mutatedAst, {
|
|
323
|
+
noScope: true,
|
|
324
|
+
enter(path) {
|
|
325
|
+
if (eqNode(path.node, original)) {
|
|
326
|
+
path.replaceWith(replacement);
|
|
327
|
+
path.stop();
|
|
328
|
+
applied = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
if (!applied) throw new Error(`Could not apply mutant ${JSON.stringify(this.replacement)}.`);
|
|
333
|
+
return mutatedAst;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function toApiLocation(source, offset) {
|
|
338
|
+
return {
|
|
339
|
+
start: toPosition(source.start, offset),
|
|
340
|
+
end: toPosition(source.end, offset)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function toPosition(source, offset) {
|
|
344
|
+
return {
|
|
345
|
+
column: source.column + (source.line === 1 ? offset.column : 0),
|
|
346
|
+
line: source.line + offset.line - 1
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/mutators/arithmetic-operator-mutator.ts
|
|
351
|
+
const arithmeticOperatorReplacements = Object.freeze({
|
|
352
|
+
"+": "-",
|
|
353
|
+
"-": "+",
|
|
354
|
+
"*": "/",
|
|
355
|
+
"/": "*",
|
|
356
|
+
"%": "*"
|
|
357
|
+
});
|
|
358
|
+
const arithmeticOperatorMutator = {
|
|
359
|
+
name: "ArithmeticOperator",
|
|
360
|
+
*mutate(path) {
|
|
361
|
+
if (path.isBinaryExpression() && isSupported$3(path.node.operator, path.node)) {
|
|
362
|
+
const mutatedOperator = arithmeticOperatorReplacements[path.node.operator];
|
|
363
|
+
const replacement = deepCloneNode(path.node);
|
|
364
|
+
replacement.operator = mutatedOperator;
|
|
365
|
+
yield replacement;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
function isSupported$3(operator, node) {
|
|
370
|
+
if (!Object.keys(arithmeticOperatorReplacements).includes(operator)) return false;
|
|
371
|
+
const stringTypes = ["StringLiteral", "TemplateLiteral"];
|
|
372
|
+
const leftType = node.left.type === "BinaryExpression" ? node.left.right.type : node.left.type;
|
|
373
|
+
if (stringTypes.includes(node.right.type) || stringTypes.includes(leftType)) return false;
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/mutators/array-declaration-mutator.ts
|
|
378
|
+
const { types: types$14 } = babel;
|
|
379
|
+
const arrayDeclarationMutator = {
|
|
380
|
+
name: "ArrayDeclaration",
|
|
381
|
+
*mutate(path) {
|
|
382
|
+
if (path.isArrayExpression()) yield path.node.elements.length ? types$14.arrayExpression() : types$14.arrayExpression([types$14.stringLiteral("Stryker was here")]);
|
|
383
|
+
if ((path.isCallExpression() || path.isNewExpression()) && types$14.isIdentifier(path.node.callee) && path.node.callee.name === "Array") {
|
|
384
|
+
const mutatedCallArgs = path.node.arguments.length ? [] : [types$14.arrayExpression()];
|
|
385
|
+
yield types$14.isNewExpression(path.node) ? types$14.newExpression(deepCloneNode(path.node.callee), mutatedCallArgs) : types$14.callExpression(deepCloneNode(path.node.callee), mutatedCallArgs);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/mutators/arrow-function-mutator.ts
|
|
391
|
+
const { types: types$13 } = babel;
|
|
392
|
+
const arrowFunctionMutator = {
|
|
393
|
+
name: "ArrowFunction",
|
|
394
|
+
*mutate(path) {
|
|
395
|
+
if (path.isArrowFunctionExpression() && !types$13.isBlockStatement(path.node.body) && !(types$13.isIdentifier(path.node.body) && path.node.body.name === "undefined")) yield types$13.arrowFunctionExpression([], types$13.identifier("undefined"));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/mutators/assignment-operator-mutator.ts
|
|
400
|
+
const assignmentOperatorReplacements = Object.freeze({
|
|
401
|
+
"+=": "-=",
|
|
402
|
+
"-=": "+=",
|
|
403
|
+
"*=": "/=",
|
|
404
|
+
"/=": "*=",
|
|
405
|
+
"%=": "*=",
|
|
406
|
+
"<<=": ">>=",
|
|
407
|
+
">>=": "<<=",
|
|
408
|
+
"&=": "|=",
|
|
409
|
+
"|=": "&=",
|
|
410
|
+
"&&=": "||=",
|
|
411
|
+
"||=": "&&=",
|
|
412
|
+
"??=": "&&="
|
|
413
|
+
});
|
|
414
|
+
const stringTypes = Object.freeze(["StringLiteral", "TemplateLiteral"]);
|
|
415
|
+
const stringAssignmentTypes = Object.freeze([
|
|
416
|
+
"&&=",
|
|
417
|
+
"||=",
|
|
418
|
+
"??="
|
|
419
|
+
]);
|
|
420
|
+
const assignmentOperatorMutator = {
|
|
421
|
+
name: "AssignmentOperator",
|
|
422
|
+
*mutate(path) {
|
|
423
|
+
if (path.isAssignmentExpression() && isSupportedAssignmentOperator(path.node.operator) && isSupported$2(path.node)) {
|
|
424
|
+
const mutatedOperator = assignmentOperatorReplacements[path.node.operator];
|
|
425
|
+
const replacement = deepCloneNode(path.node);
|
|
426
|
+
replacement.operator = mutatedOperator;
|
|
427
|
+
yield replacement;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
function isSupportedAssignmentOperator(operator) {
|
|
432
|
+
return Object.keys(assignmentOperatorReplacements).includes(operator);
|
|
433
|
+
}
|
|
434
|
+
function isSupported$2(node) {
|
|
435
|
+
if (stringTypes.includes(node.right.type) && !stringAssignmentTypes.includes(node.operator)) return false;
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/mutators/block-statement-mutator.ts
|
|
440
|
+
const { types: types$12 } = babel;
|
|
441
|
+
const blockStatementMutator = {
|
|
442
|
+
name: "BlockStatement",
|
|
443
|
+
*mutate(path) {
|
|
444
|
+
if (path.isBlockStatement() && isValid(path)) yield types$12.blockStatement([]);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
function isValid(path) {
|
|
448
|
+
return !isEmpty(path) && !isInvalidConstructorBody(path);
|
|
449
|
+
}
|
|
450
|
+
function isEmpty(path) {
|
|
451
|
+
return !path.node.body.length;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Checks to see if a statement is an invalid constructor body
|
|
455
|
+
* @example
|
|
456
|
+
* // Invalid!
|
|
457
|
+
* class Foo extends Bar {
|
|
458
|
+
* constructor(public baz: string) {
|
|
459
|
+
* super(42);
|
|
460
|
+
* }
|
|
461
|
+
* }
|
|
462
|
+
* @example
|
|
463
|
+
* // Invalid!
|
|
464
|
+
* class Foo extends Bar {
|
|
465
|
+
* public baz = 'string';
|
|
466
|
+
* constructor() {
|
|
467
|
+
* super(42);
|
|
468
|
+
* }
|
|
469
|
+
* }
|
|
470
|
+
* @see https://github.com/stryker-mutator/stryker-js/issues/2314
|
|
471
|
+
* @see https://github.com/stryker-mutator/stryker-js/issues/2474
|
|
472
|
+
*/
|
|
473
|
+
function isInvalidConstructorBody(blockStatement) {
|
|
474
|
+
return Boolean(blockStatement.parentPath.isClassMethod() && blockStatement.parentPath.node.kind === "constructor" && (containsTSParameterProperties(blockStatement.parentPath) || containsInitializedClassProperties(blockStatement.parentPath)) && hasSuperExpression(blockStatement));
|
|
475
|
+
}
|
|
476
|
+
function containsTSParameterProperties(constructor) {
|
|
477
|
+
return constructor.node.params.some((param) => types$12.isTSParameterProperty(param));
|
|
478
|
+
}
|
|
479
|
+
function containsInitializedClassProperties(constructor) {
|
|
480
|
+
return constructor.parentPath.isClassBody() && constructor.parentPath.node.body.some((classMember) => types$12.isClassProperty(classMember) && classMember.value);
|
|
481
|
+
}
|
|
482
|
+
function hasSuperExpression(constructor) {
|
|
483
|
+
let hasSuper = false;
|
|
484
|
+
constructor.traverse({ Super(path) {
|
|
485
|
+
if (path.parentPath.isCallExpression()) {
|
|
486
|
+
path.stop();
|
|
487
|
+
hasSuper = true;
|
|
488
|
+
}
|
|
489
|
+
} });
|
|
490
|
+
return hasSuper;
|
|
491
|
+
}
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/mutators/boolean-literal-mutator.ts
|
|
494
|
+
const { types: types$11 } = babel;
|
|
495
|
+
const booleanLiteralMutator = {
|
|
496
|
+
name: "BooleanLiteral",
|
|
497
|
+
*mutate(path) {
|
|
498
|
+
if (path.isBooleanLiteral()) yield types$11.booleanLiteral(!path.node.value);
|
|
499
|
+
if (path.isUnaryExpression() && path.node.operator === "!" && path.node.prefix) yield deepCloneNode(path.node.argument);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/mutators/conditional-expression-mutator.ts
|
|
504
|
+
const booleanOperators = Object.freeze([
|
|
505
|
+
"!=",
|
|
506
|
+
"!==",
|
|
507
|
+
"&&",
|
|
508
|
+
"<",
|
|
509
|
+
"<=",
|
|
510
|
+
"==",
|
|
511
|
+
"===",
|
|
512
|
+
">",
|
|
513
|
+
">=",
|
|
514
|
+
"||"
|
|
515
|
+
]);
|
|
516
|
+
const { types: types$10 } = babel;
|
|
517
|
+
const conditionalExpressionMutator = {
|
|
518
|
+
name: "ConditionalExpression",
|
|
519
|
+
*mutate(path) {
|
|
520
|
+
if (isTestOfLoop(path)) yield types$10.booleanLiteral(false);
|
|
521
|
+
else if (isTestOfCondition(path)) {
|
|
522
|
+
yield types$10.booleanLiteral(true);
|
|
523
|
+
yield types$10.booleanLiteral(false);
|
|
524
|
+
} else if (isBooleanExpression(path)) {
|
|
525
|
+
if (path.parent?.type === "LogicalExpression") {
|
|
526
|
+
if (path.parent.operator === "||") {
|
|
527
|
+
yield types$10.booleanLiteral(false);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (path.parent.operator === "&&") {
|
|
531
|
+
yield types$10.booleanLiteral(true);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
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
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
function isTestOfLoop(path) {
|
|
549
|
+
const { parentPath } = path;
|
|
550
|
+
if (!parentPath) return false;
|
|
551
|
+
return (parentPath.isForStatement() || parentPath.isWhileStatement() || parentPath.isDoWhileStatement()) && parentPath.node.test === path.node;
|
|
552
|
+
}
|
|
553
|
+
function isTestOfCondition(path) {
|
|
554
|
+
const { parentPath } = path;
|
|
555
|
+
if (!parentPath) return false;
|
|
556
|
+
return parentPath.isIfStatement() && parentPath.node.test === path.node;
|
|
557
|
+
}
|
|
558
|
+
function isBooleanExpression(path) {
|
|
559
|
+
return (path.isBinaryExpression() || path.isLogicalExpression()) && booleanOperators.includes(path.node.operator);
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/mutators/equality-operator-mutator.ts
|
|
563
|
+
const { types: t$2 } = babel;
|
|
564
|
+
const operators = {
|
|
565
|
+
"<": ["<=", ">="],
|
|
566
|
+
"<=": ["<", ">"],
|
|
567
|
+
">": [">=", "<="],
|
|
568
|
+
">=": [">", "<"],
|
|
569
|
+
"==": ["!="],
|
|
570
|
+
"!=": ["=="],
|
|
571
|
+
"===": ["!=="],
|
|
572
|
+
"!==": ["==="]
|
|
573
|
+
};
|
|
574
|
+
function isEqualityOperator(operator) {
|
|
575
|
+
return Object.keys(operators).includes(operator);
|
|
576
|
+
}
|
|
577
|
+
const equalityOperatorMutator = {
|
|
578
|
+
name: "EqualityOperator",
|
|
579
|
+
*mutate(path) {
|
|
580
|
+
if (path.isBinaryExpression() && isEqualityOperator(path.node.operator)) for (const mutableOperator of operators[path.node.operator]) {
|
|
581
|
+
const replacement = t$2.cloneNode(path.node, true);
|
|
582
|
+
replacement.operator = mutableOperator;
|
|
583
|
+
yield replacement;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/mutators/logical-operator-mutator.ts
|
|
589
|
+
const logicalOperatorReplacements = Object.freeze({
|
|
590
|
+
"&&": "||",
|
|
591
|
+
"||": "&&",
|
|
592
|
+
"??": "&&"
|
|
593
|
+
});
|
|
594
|
+
const logicalOperatorMutator = {
|
|
595
|
+
name: "LogicalOperator",
|
|
596
|
+
*mutate(path) {
|
|
597
|
+
if (path.isLogicalExpression() && isSupported$1(path.node.operator)) {
|
|
598
|
+
const mutatedOperator = logicalOperatorReplacements[path.node.operator];
|
|
599
|
+
const replacement = deepCloneNode(path.node);
|
|
600
|
+
replacement.operator = mutatedOperator;
|
|
601
|
+
yield replacement;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
function isSupported$1(operator) {
|
|
606
|
+
return Object.keys(logicalOperatorReplacements).includes(operator);
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/mutators/method-expression-mutator.ts
|
|
610
|
+
const { types: types$9 } = babel;
|
|
611
|
+
const replacements = /* @__PURE__ */ new Map([
|
|
612
|
+
["charAt", null],
|
|
613
|
+
["endsWith", "startsWith"],
|
|
614
|
+
["every", "some"],
|
|
615
|
+
["filter", null],
|
|
616
|
+
["reverse", null],
|
|
617
|
+
["slice", null],
|
|
618
|
+
["sort", null],
|
|
619
|
+
["substr", null],
|
|
620
|
+
["substring", null],
|
|
621
|
+
["toLocaleLowerCase", "toLocaleUpperCase"],
|
|
622
|
+
["toLowerCase", "toUpperCase"],
|
|
623
|
+
["trim", null],
|
|
624
|
+
["trimEnd", "trimStart"],
|
|
625
|
+
["min", "max"],
|
|
626
|
+
["setDate", "setTime"],
|
|
627
|
+
["setFullYear", "setMonth"],
|
|
628
|
+
["setHours", "setMinutes"],
|
|
629
|
+
["setSeconds", "setMilliseconds"],
|
|
630
|
+
["setUTCDate", "setTime"],
|
|
631
|
+
["setUTCFullYear", "setUTCMonth"],
|
|
632
|
+
["setUTCHours", "setUTCMinutes"],
|
|
633
|
+
["setUTCSeconds", "setUTCMilliseconds"]
|
|
634
|
+
]);
|
|
635
|
+
const noReverseRemplacements = ["getUTCDate", "setUTCDate"];
|
|
636
|
+
for (const [key, value] of Array.from(replacements)) if (value && !noReverseRemplacements.includes(key)) replacements.set(value, key);
|
|
637
|
+
const methodExpressionMutator = {
|
|
638
|
+
name: "MethodExpression",
|
|
639
|
+
*mutate(path) {
|
|
640
|
+
if (!(path.isCallExpression() || path.isOptionalCallExpression())) return;
|
|
641
|
+
const { callee } = path.node;
|
|
642
|
+
if (!(types$9.isMemberExpression(callee) || types$9.isOptionalMemberExpression(callee)) || !types$9.isIdentifier(callee.property)) return;
|
|
643
|
+
const newName = replacements.get(callee.property.name);
|
|
644
|
+
if (newName === void 0) return;
|
|
645
|
+
if (newName === null) {
|
|
646
|
+
yield deepCloneNode(callee.object);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const nodeArguments = path.node.arguments.map((argumentNode) => deepCloneNode(argumentNode));
|
|
650
|
+
const mutatedCallee = types$9.isMemberExpression(callee) ? types$9.memberExpression(deepCloneNode(callee.object), types$9.identifier(newName), false, callee.optional) : types$9.optionalMemberExpression(deepCloneNode(callee.object), types$9.identifier(newName), false, callee.optional);
|
|
651
|
+
yield types$9.isCallExpression(path.node) ? types$9.callExpression(mutatedCallee, nodeArguments) : types$9.optionalCallExpression(mutatedCallee, nodeArguments, path.node.optional);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/mutators/object-literal-mutator.ts
|
|
656
|
+
const { types: types$8 } = babel;
|
|
657
|
+
const objectLiteralMutator = {
|
|
658
|
+
name: "ObjectLiteral",
|
|
659
|
+
*mutate(path) {
|
|
660
|
+
if (path.isObjectExpression() && path.node.properties.length > 0) yield types$8.objectExpression([]);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/mutators/optional-chaining-mutator.ts
|
|
665
|
+
const { types: t$1 } = babel;
|
|
666
|
+
/**
|
|
667
|
+
* Mutates optional chaining operators
|
|
668
|
+
* Note that the AST for optional chaining might not be what you expect. Nodes of type `OptionalMemberExpression` can be either optional or not-optional
|
|
669
|
+
*
|
|
670
|
+
* For example: In this expression: `foo?.bar.baz` the `.baz` member expression is of type `OptionalMemberExpression`, because it is part of an optional chain, but is is _not_ optional.
|
|
671
|
+
* Only the `.bar` optional member expression is optional.
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* foo?.bar -> foo.bar
|
|
675
|
+
* foo?.[1] -> foo[1]
|
|
676
|
+
* foo?.() -> foo()
|
|
677
|
+
*/
|
|
678
|
+
const optionalChainingMutator = {
|
|
679
|
+
name: "OptionalChaining",
|
|
680
|
+
*mutate(path) {
|
|
681
|
+
if (path.isOptionalMemberExpression() && path.node.optional) yield t$1.optionalMemberExpression(t$1.cloneNode(path.node.object, true), t$1.cloneNode(path.node.property, true), path.node.computed, false);
|
|
682
|
+
if (path.isOptionalCallExpression() && path.node.optional) yield t$1.optionalCallExpression(t$1.cloneNode(path.node.callee, true), path.node.arguments.map((arg) => t$1.cloneNode(arg, true)), false);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/mutators/regex-mutator.ts
|
|
687
|
+
const { types: types$7 } = babel;
|
|
688
|
+
/**
|
|
689
|
+
* Checks that a string literal is an obvious regex string literal
|
|
690
|
+
* @param path The string literal to checks
|
|
691
|
+
* @example
|
|
692
|
+
* new RegExp("\\d{4}");
|
|
693
|
+
*/
|
|
694
|
+
function isObviousRegexString(path) {
|
|
695
|
+
return path.parentPath.isNewExpression() && types$7.isIdentifier(path.parentPath.node.callee) && path.parentPath.node.callee.name === RegExp.name && path.parentPath.node.arguments[0] === path.node;
|
|
696
|
+
}
|
|
697
|
+
function getFlags(path) {
|
|
698
|
+
if (types$7.isStringLiteral(path.node.arguments[1])) return path.node.arguments[1].value;
|
|
699
|
+
}
|
|
700
|
+
const weaponRegexOptions = { mutationLevels: [1] };
|
|
701
|
+
const regexMutator = {
|
|
702
|
+
name: "Regex",
|
|
703
|
+
*mutate(path) {
|
|
704
|
+
if (path.isRegExpLiteral()) for (const replacementPattern of mutatePattern(path.node.pattern, path.node.flags)) yield types$7.regExpLiteral(replacementPattern, path.node.flags);
|
|
705
|
+
else if (path.isStringLiteral() && isObviousRegexString(path)) {
|
|
706
|
+
const parentPath = path.parentPath;
|
|
707
|
+
if (parentPath.isNewExpression()) {
|
|
708
|
+
const flags = getFlags(parentPath);
|
|
709
|
+
for (const replacementPattern of mutatePattern(path.node.value, flags)) yield types$7.stringLiteral(replacementPattern);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
function mutatePattern(pattern, flags) {
|
|
715
|
+
if (pattern.length) try {
|
|
716
|
+
return weaponRegex.mutate(pattern, flags, weaponRegexOptions).map((mutant) => mutant.pattern);
|
|
717
|
+
} catch (err) {
|
|
718
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err) ?? "Unknown error";
|
|
719
|
+
console.error(`[RegexMutator]: The Regex parser of weapon-regex couldn't parse this regex pattern: "${pattern}". Please report this issue at https://github.com/stryker-mutator/weapon-regex/issues. Inner error: ${message}`);
|
|
720
|
+
}
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/mutators/string-literal-mutator.ts
|
|
725
|
+
const { types: types$6 } = babel;
|
|
726
|
+
const stringLiteralMutator = {
|
|
727
|
+
name: "StringLiteral",
|
|
728
|
+
*mutate(path) {
|
|
729
|
+
if (path.isTemplateLiteral()) {
|
|
730
|
+
const firstQuasi = path.node.quasis[0];
|
|
731
|
+
if (firstQuasi === void 0) throw new Error("Template literal without quasis");
|
|
732
|
+
const replacement = path.node.quasis.length === 1 && firstQuasi.value.raw.length === 0 ? "Stryker was here!" : "";
|
|
733
|
+
yield types$6.templateLiteral([types$6.templateElement({ raw: replacement })], []);
|
|
734
|
+
}
|
|
735
|
+
if (path.isStringLiteral() && isValidParent(path)) yield types$6.stringLiteral(path.node.value.length === 0 ? "Stryker was here!" : "");
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
function isValidParent(child) {
|
|
739
|
+
const { parent } = child;
|
|
740
|
+
return !isImportExportRelated(parent) && !isJsxOrExpressionRelated(parent) && !isObjectOrClassPropertyKey(parent, child) && !isDisallowedCallExpression(parent);
|
|
741
|
+
}
|
|
742
|
+
function isImportExportRelated(parent) {
|
|
743
|
+
return types$6.isImportDeclaration(parent) || types$6.isExportDeclaration(parent) || types$6.isImportOrExportDeclaration(parent) || types$6.isTSExternalModuleReference(parent);
|
|
744
|
+
}
|
|
745
|
+
function isJsxOrExpressionRelated(parent) {
|
|
746
|
+
return types$6.isJSXAttribute(parent) || types$6.isExpressionStatement(parent) || types$6.isTSLiteralType(parent) || types$6.isObjectMethod(parent);
|
|
747
|
+
}
|
|
748
|
+
function isObjectOrClassPropertyKey(parent, child) {
|
|
749
|
+
return types$6.isObjectProperty(parent) && parent.key === child.node || types$6.isClassProperty(parent) && parent.key === child.node;
|
|
750
|
+
}
|
|
751
|
+
function isDisallowedCallExpression(parent) {
|
|
752
|
+
return isRequireCall(parent) || isSymbolCall(parent) || isImportCall(parent);
|
|
753
|
+
}
|
|
754
|
+
function isRequireCall(parent) {
|
|
755
|
+
return types$6.isCallExpression(parent) && types$6.isIdentifier(parent.callee, { name: "require" });
|
|
756
|
+
}
|
|
757
|
+
function isSymbolCall(parent) {
|
|
758
|
+
return types$6.isCallExpression(parent) && types$6.isIdentifier(parent.callee, { name: "Symbol" });
|
|
759
|
+
}
|
|
760
|
+
function isImportCall(parent) {
|
|
761
|
+
return types$6.isCallExpression(parent) && types$6.isImport(parent.callee);
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/mutators/unary-operator-mutator.ts
|
|
765
|
+
const { types: types$5 } = babel;
|
|
766
|
+
var UnaryOperator = /* @__PURE__ */ function(UnaryOperator) {
|
|
767
|
+
UnaryOperator["+"] = "-";
|
|
768
|
+
UnaryOperator["-"] = "+";
|
|
769
|
+
UnaryOperator["~"] = "";
|
|
770
|
+
return UnaryOperator;
|
|
771
|
+
}(UnaryOperator || {});
|
|
772
|
+
const unaryOperatorMutator = {
|
|
773
|
+
name: "UnaryOperator",
|
|
774
|
+
*mutate(path) {
|
|
775
|
+
if (path.isUnaryExpression() && isSupported(path.node.operator) && path.node.prefix) {
|
|
776
|
+
const mutatedOperator = UnaryOperator[path.node.operator];
|
|
777
|
+
yield isPlusOrMinus(mutatedOperator) ? types$5.unaryExpression(mutatedOperator, deepCloneNode(path.node.argument)) : deepCloneNode(path.node.argument);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
function isSupported(operator) {
|
|
782
|
+
return Object.keys(UnaryOperator).includes(operator);
|
|
783
|
+
}
|
|
784
|
+
function isPlusOrMinus(operator) {
|
|
785
|
+
return operator === "-" || operator === "+";
|
|
786
|
+
}
|
|
787
|
+
//#endregion
|
|
788
|
+
//#region src/mutators/update-operator-mutator.ts
|
|
789
|
+
const { types: types$4 } = babel;
|
|
790
|
+
var UpdateOperators = /* @__PURE__ */ function(UpdateOperators) {
|
|
791
|
+
UpdateOperators["++"] = "--";
|
|
792
|
+
UpdateOperators["--"] = "++";
|
|
793
|
+
return UpdateOperators;
|
|
794
|
+
}(UpdateOperators || {});
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/mutators/mutate.ts
|
|
797
|
+
const allMutators = [
|
|
798
|
+
arithmeticOperatorMutator,
|
|
799
|
+
arrayDeclarationMutator,
|
|
800
|
+
arrowFunctionMutator,
|
|
801
|
+
blockStatementMutator,
|
|
802
|
+
booleanLiteralMutator,
|
|
803
|
+
conditionalExpressionMutator,
|
|
804
|
+
equalityOperatorMutator,
|
|
805
|
+
logicalOperatorMutator,
|
|
806
|
+
methodExpressionMutator,
|
|
807
|
+
objectLiteralMutator,
|
|
808
|
+
stringLiteralMutator,
|
|
809
|
+
unaryOperatorMutator,
|
|
810
|
+
{
|
|
811
|
+
name: "UpdateOperator",
|
|
812
|
+
*mutate(path) {
|
|
813
|
+
if (path.isUpdateExpression()) yield types$4.updateExpression(UpdateOperators[path.node.operator], deepCloneNode(path.node.argument), path.node.prefix);
|
|
814
|
+
}
|
|
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
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Adds mutants to the internal mutant list.
|
|
829
|
+
* @param fileName file name that houses the mutant
|
|
830
|
+
* @param original The node to mutate
|
|
831
|
+
* @param mutables the named node mutation to be added
|
|
832
|
+
* @param contextPath the context where these mutants are found and should be placed as close by as possible
|
|
833
|
+
* @param offset offset of mutant nodes
|
|
834
|
+
* @returns The mutant (for testability)
|
|
835
|
+
*/
|
|
836
|
+
collect(fileName, original, mutable, offset = {
|
|
837
|
+
line: 0,
|
|
838
|
+
column: 0
|
|
839
|
+
}) {
|
|
840
|
+
const mutant = new Mutant$1(this._mutants.length.toString(), fileName, original, mutable, offset);
|
|
841
|
+
this._mutants.push(mutant);
|
|
842
|
+
return mutant;
|
|
843
|
+
}
|
|
844
|
+
hasPlacedMutants(fileName) {
|
|
845
|
+
return this.mutants.some((mutant) => mutant.fileName === fileName && !mutant.ignoreReason);
|
|
846
|
+
}
|
|
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
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/mutant-placers/expression-mutant-placer.ts
|
|
862
|
+
const { types: types$3 } = babel;
|
|
863
|
+
/**
|
|
864
|
+
* Will set the identifier of anonymous function expressions if is located in a variable declaration.
|
|
865
|
+
* Will treat input as readonly. Returns undefined if not needed.
|
|
866
|
+
* @example
|
|
867
|
+
* const a = function() { }
|
|
868
|
+
* becomes
|
|
869
|
+
* const a = function a() {}
|
|
870
|
+
*/
|
|
871
|
+
function classOrFunctionExpressionNamedIfNeeded(path) {
|
|
872
|
+
if ((path.isFunctionExpression() || path.isClassExpression()) && !path.node.id) {
|
|
873
|
+
if (path.parentPath.isVariableDeclarator() && types$3.isIdentifier(path.parentPath.node.id)) {
|
|
874
|
+
path.node.id = path.parentPath.node.id;
|
|
875
|
+
return path.node;
|
|
876
|
+
} else if (path.parentPath.isObjectProperty() && types$3.isIdentifier(path.parentPath.node.key) && path.getStatementParent()?.isVariableDeclaration()) {
|
|
877
|
+
path.node.id = path.parentPath.node.key;
|
|
878
|
+
return path.node;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Will set the identifier of anonymous arrow function expressions if is located in a variable declaration.
|
|
884
|
+
* Will treat input as readonly. Returns undefined if not needed.
|
|
885
|
+
* @example
|
|
886
|
+
* const a = () => { }
|
|
887
|
+
* becomes
|
|
888
|
+
* const a = (() => { const a = () => {}; return a; })()
|
|
889
|
+
*/
|
|
890
|
+
function arrowFunctionExpressionNamedIfNeeded(path) {
|
|
891
|
+
if (path.isArrowFunctionExpression() && path.parentPath.isVariableDeclarator() && types$3.isIdentifier(path.parentPath.node.id)) return types$3.callExpression(types$3.arrowFunctionExpression([], types$3.blockStatement([types$3.variableDeclaration("const", [types$3.variableDeclarator(path.parentPath.node.id, path.node)]), types$3.returnStatement(path.parentPath.node.id)])), []);
|
|
892
|
+
}
|
|
893
|
+
function nameIfAnonymous(path) {
|
|
894
|
+
return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
|
|
895
|
+
}
|
|
896
|
+
function isMemberOrCallOrNonNullExpression(path) {
|
|
897
|
+
return isCallExpression(path) || isMemberOrNonNullExpression(path);
|
|
898
|
+
}
|
|
899
|
+
function isMemberOrNonNullExpression(path) {
|
|
900
|
+
return isMemberExpression(path) || path.isTSNonNullExpression();
|
|
901
|
+
}
|
|
902
|
+
function isMemberExpression(path) {
|
|
903
|
+
return path.isMemberExpression() || path.isOptionalMemberExpression();
|
|
904
|
+
}
|
|
905
|
+
function isCallExpression(path) {
|
|
906
|
+
return path.isCallExpression() || path.isOptionalCallExpression();
|
|
907
|
+
}
|
|
908
|
+
function isValidExpression(path) {
|
|
909
|
+
const parent = path.parentPath;
|
|
910
|
+
return !isObjectPropertyKey() && !isPartOfChain() && !parent.isTaggedTemplateExpression() && !isPartOfDeleteExpression() && !isPartOfAssignmentExpression();
|
|
911
|
+
/**
|
|
912
|
+
* Determines if the expression is property of an object.
|
|
913
|
+
* @example
|
|
914
|
+
* const a = {
|
|
915
|
+
* 'foo': 'bar' // 'foo' here is an object property
|
|
916
|
+
* };
|
|
917
|
+
*/
|
|
918
|
+
function isObjectPropertyKey() {
|
|
919
|
+
return parent.isObjectProperty() && parent.node.key === path.node;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Determines if the expression is part of a call/member chain.
|
|
923
|
+
* @example
|
|
924
|
+
* // bar is part of chain, foo is NOT part of the chain:
|
|
925
|
+
* foo.bar.baz();
|
|
926
|
+
* foo.bar?.baz()
|
|
927
|
+
* foo.bar;
|
|
928
|
+
* foo.bar!;
|
|
929
|
+
* foo.bar();
|
|
930
|
+
* foo?.bar();
|
|
931
|
+
* baz[foo.bar()]
|
|
932
|
+
* bar?.baz[0]
|
|
933
|
+
*/
|
|
934
|
+
function isPartOfChain() {
|
|
935
|
+
return isMemberOrCallOrNonNullExpression(path) && (isMemberExpression(parent) && !(parent.node.computed && parent.node.property === path.node) || parent.isTSNonNullExpression() || isCallExpression(parent) && parent.node.callee === path.node);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Determines if the expression is part of a delete expression.
|
|
939
|
+
* @returns true if the expression is part of a delete expression
|
|
940
|
+
* @example
|
|
941
|
+
* delete foo.bar;
|
|
942
|
+
*/
|
|
943
|
+
function isPartOfDeleteExpression() {
|
|
944
|
+
return parent.isUnaryExpression() && parent.node.operator === "delete";
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Determines if the expression is part of an assignment expression.
|
|
948
|
+
* @returns true if the expression is part of an assignment expression
|
|
949
|
+
* @example
|
|
950
|
+
* foo.bar = 42;
|
|
951
|
+
* initialNodes.filter((n) => n.id === 'tiptilt')[0].className = tiptiltState;
|
|
952
|
+
*/
|
|
953
|
+
function isPartOfAssignmentExpression() {
|
|
954
|
+
return parent.isAssignmentExpression() && parent.node.left === path.node;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : originalCode`;
|
|
959
|
+
*/
|
|
960
|
+
const expressionMutantPlacer = {
|
|
961
|
+
name: "expressionMutantPlacer",
|
|
962
|
+
canPlace(path) {
|
|
963
|
+
return path.isExpression() && isValidExpression(path);
|
|
964
|
+
},
|
|
965
|
+
place(path, appliedMutants) {
|
|
966
|
+
let expression = nameIfAnonymous(path);
|
|
967
|
+
expression = mutationCoverageSequenceExpression(appliedMutants.keys(), expression);
|
|
968
|
+
for (const [mutant, appliedMutant] of appliedMutants) expression = types$3.conditionalExpression(mutantTestExpression(mutant.id), nodeOfKind(mutant, appliedMutant, types$3.isExpression, "an expression"), expression);
|
|
969
|
+
path.replaceWith(expression);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/mutant-placers/statement-mutant-placer.ts
|
|
974
|
+
const { types: t } = babel;
|
|
975
|
+
/**
|
|
976
|
+
* Mutant placer that places mutants in statements that allow it.
|
|
977
|
+
* It uses an `if` statement to do so
|
|
978
|
+
*/
|
|
979
|
+
const statementMutantPlacer = {
|
|
980
|
+
name: "statementMutantPlacer",
|
|
981
|
+
canPlace(path) {
|
|
982
|
+
return path.isStatement();
|
|
983
|
+
},
|
|
984
|
+
place(path, appliedMutants) {
|
|
985
|
+
let statement = t.blockStatement([t.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.isBlockStatement() ? path.node.body : [path.node]]);
|
|
986
|
+
for (const [mutant, appliedMutant] of appliedMutants) statement = t.ifStatement(mutantTestExpression(mutant.id), t.blockStatement([nodeOfKind(mutant, appliedMutant, t.isStatement, "a statement")]), statement);
|
|
987
|
+
path.replaceWith(path.isBlockStatement() ? t.blockStatement([statement]) : statement);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/mutant-placers/switch-case-mutant-placer.ts
|
|
992
|
+
/**
|
|
993
|
+
* Places the mutants with consequent of a SwitchCase node. Uses an if-statement to do so.
|
|
994
|
+
* @example
|
|
995
|
+
* case 'foo':
|
|
996
|
+
* if (stryMutAct_9fa48(0)) {} else {
|
|
997
|
+
* stryCov_9fa48(0);
|
|
998
|
+
* console.log('bar');
|
|
999
|
+
* break;
|
|
1000
|
+
* }
|
|
1001
|
+
*/
|
|
1002
|
+
const switchCaseMutantPlacer = {
|
|
1003
|
+
name: "switchCaseMutantPlacer",
|
|
1004
|
+
canPlace(path) {
|
|
1005
|
+
return path.isSwitchCase();
|
|
1006
|
+
},
|
|
1007
|
+
place(path, appliedMutants) {
|
|
1008
|
+
let consequence = babel.types.blockStatement([babel.types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())), ...path.node.consequent]);
|
|
1009
|
+
for (const [mutant, appliedMutant] of appliedMutants) {
|
|
1010
|
+
const switchCase = nodeOfKind(mutant, appliedMutant, babel.types.isSwitchCase, "a switch case");
|
|
1011
|
+
consequence = babel.types.ifStatement(mutantTestExpression(mutant.id), babel.types.blockStatement(switchCase.consequent), consequence);
|
|
1012
|
+
}
|
|
1013
|
+
path.replaceWith(babel.types.switchCase(path.node.test, [consequence]));
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/mutant-placers/throw-placement-error.ts
|
|
1018
|
+
function throwPlacementError(error, nodePath, placer, mutants, fileName) {
|
|
1019
|
+
const location = `${path.relative(process.cwd(), fileName)}:${nodePath.node.loc?.start.line}:${nodePath.node.loc?.start.column}`;
|
|
1020
|
+
const message = `${placer.name} could not place mutants with type(s): "${new Intl.ListFormat("en").format(mutants.map((mutant) => mutant.mutatorName))}"`;
|
|
1021
|
+
const errorMessage = `${location} ${message}. Either remove this file from the list of files to be mutated, or exclude the mutator (using ${propertyPath()("mutator", "excludedMutations")}). Please report this issue at https://github.com/stryker-mutator/stryker-js/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug_report.md&title=${encodeURIComponent(message)}. Original error: ${error.stack}`;
|
|
1022
|
+
let builtError = new Error(errorMessage);
|
|
1023
|
+
try {
|
|
1024
|
+
builtError = nodePath.buildCodeFrameError(errorMessage);
|
|
1025
|
+
} catch {}
|
|
1026
|
+
throw builtError;
|
|
1027
|
+
}
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/mutant-placers/index.ts
|
|
1030
|
+
const allMutantPlacers = Object.freeze([
|
|
1031
|
+
expressionMutantPlacer,
|
|
1032
|
+
statementMutantPlacer,
|
|
1033
|
+
switchCaseMutantPlacer
|
|
1034
|
+
]);
|
|
1035
|
+
//#endregion
|
|
1036
|
+
//#region src/util/babel-file.ts
|
|
1037
|
+
/**
|
|
1038
|
+
* Wraps a parsed AST the way Babel's own pipeline does, so
|
|
1039
|
+
* `NodePath#buildCodeFrameError` can render a code frame
|
|
1040
|
+
* (https://github.com/babel/babel/issues/11889). Without the wrapper a
|
|
1041
|
+
* placement failure reports no source context.
|
|
1042
|
+
*/
|
|
1043
|
+
function createBabelFile(filename, code, ast) {
|
|
1044
|
+
return new File({ filename }, {
|
|
1045
|
+
code,
|
|
1046
|
+
ast
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/transformers/directive-bookkeeper.ts
|
|
1051
|
+
const WILDCARD = "all";
|
|
1052
|
+
const DEFAULT_REASON = "Ignored using a comment";
|
|
1053
|
+
var IgnoreRule = class {
|
|
1054
|
+
mutatorNames;
|
|
1055
|
+
line;
|
|
1056
|
+
ignoreReason;
|
|
1057
|
+
previousRule;
|
|
1058
|
+
constructor(mutatorNames, line, ignoreReason, previousRule) {
|
|
1059
|
+
this.mutatorNames = mutatorNames;
|
|
1060
|
+
this.line = line;
|
|
1061
|
+
this.ignoreReason = ignoreReason;
|
|
1062
|
+
this.previousRule = previousRule;
|
|
1063
|
+
}
|
|
1064
|
+
matches(mutatorName, line) {
|
|
1065
|
+
const lineMatches = () => this.line === void 0 || this.line === line;
|
|
1066
|
+
const mutatorMatches = () => this.mutatorNames.includes(mutatorName) || this.mutatorNames.includes(WILDCARD);
|
|
1067
|
+
return lineMatches() && mutatorMatches();
|
|
1068
|
+
}
|
|
1069
|
+
findIgnoreReason(mutatorName, line) {
|
|
1070
|
+
if (this.matches(mutatorName, line)) return this.ignoreReason;
|
|
1071
|
+
return this.previousRule.findIgnoreReason(mutatorName, line);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var RestoreRule = class extends IgnoreRule {
|
|
1075
|
+
constructor(mutatorNames, line, previousRule) {
|
|
1076
|
+
super(mutatorNames, line, void 0, previousRule);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
const rootRule = { findIgnoreReason() {} };
|
|
1080
|
+
/**
|
|
1081
|
+
* Responsible for the bookkeeping of "// Stryker" directives like "disable" and "restore".
|
|
1082
|
+
*/
|
|
1083
|
+
var DirectiveBookkeeper = class {
|
|
1084
|
+
logger;
|
|
1085
|
+
allMutators;
|
|
1086
|
+
originFileName;
|
|
1087
|
+
strykerCommentDirectiveRegex = /^\s?Stryker (disable|restore)(?: (next-line))? ([a-zA-Z, ]+)(?::(.+)?)?/;
|
|
1088
|
+
currentIgnoreRule = rootRule;
|
|
1089
|
+
allMutatorNames;
|
|
1090
|
+
constructor(logger, allMutators, originFileName) {
|
|
1091
|
+
this.logger = logger;
|
|
1092
|
+
this.allMutators = allMutators;
|
|
1093
|
+
this.originFileName = originFileName;
|
|
1094
|
+
this.allMutatorNames = this.allMutators.map((x) => x.name.toLowerCase());
|
|
1095
|
+
}
|
|
1096
|
+
processStrykerDirectives({ loc, leadingComments }) {
|
|
1097
|
+
if (!leadingComments) return;
|
|
1098
|
+
for (const comment of leadingComments) {
|
|
1099
|
+
const matchResult = this.strykerCommentDirectiveRegex.exec(comment.value);
|
|
1100
|
+
if (!matchResult) continue;
|
|
1101
|
+
const directiveType = matchResult[1];
|
|
1102
|
+
const scope = matchResult[2];
|
|
1103
|
+
const mutators = matchResult[3];
|
|
1104
|
+
const optionalReason = matchResult[4];
|
|
1105
|
+
if (directiveType === void 0 || mutators === void 0) throw new Error("Stryker directive without directive type or mutators");
|
|
1106
|
+
let mutatorNames = mutators.split(",").map((mutator) => mutator.trim());
|
|
1107
|
+
this.warnAboutUnusedDirective(mutatorNames, directiveType, scope, comment);
|
|
1108
|
+
mutatorNames = mutatorNames.map((mutator) => mutator.toLowerCase());
|
|
1109
|
+
const reason = (optionalReason ?? DEFAULT_REASON).trim();
|
|
1110
|
+
this.applyDirective(directiveType, scope, mutatorNames, reason, loc);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
applyDirective(directiveType, scope, mutatorNames, reason, loc) {
|
|
1114
|
+
switch (directiveType) {
|
|
1115
|
+
case "disable":
|
|
1116
|
+
this.applyDisable(scope, mutatorNames, reason, loc);
|
|
1117
|
+
break;
|
|
1118
|
+
case "restore": this.applyRestore(scope, mutatorNames, loc);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
applyDisable(scope, mutatorNames, reason, loc) {
|
|
1122
|
+
switch (scope) {
|
|
1123
|
+
case "next-line":
|
|
1124
|
+
this.currentIgnoreRule = new IgnoreRule(mutatorNames, this.getLine(loc), reason, this.currentIgnoreRule);
|
|
1125
|
+
break;
|
|
1126
|
+
case void 0:
|
|
1127
|
+
default: this.currentIgnoreRule = new IgnoreRule(mutatorNames, void 0, reason, this.currentIgnoreRule);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
applyRestore(scope, mutatorNames, loc) {
|
|
1131
|
+
switch (scope) {
|
|
1132
|
+
case "next-line":
|
|
1133
|
+
this.currentIgnoreRule = new RestoreRule(mutatorNames, this.getLine(loc), this.currentIgnoreRule);
|
|
1134
|
+
break;
|
|
1135
|
+
case void 0:
|
|
1136
|
+
default: this.currentIgnoreRule = new RestoreRule(mutatorNames, void 0, this.currentIgnoreRule);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
getLine(loc) {
|
|
1140
|
+
if (loc === void 0 || loc === null || loc.start === null || loc.start === void 0) throw new Error("Babel node without location");
|
|
1141
|
+
return loc.start.line;
|
|
1142
|
+
}
|
|
1143
|
+
findIgnoreReason(line, mutatorName) {
|
|
1144
|
+
mutatorName = mutatorName.toLowerCase();
|
|
1145
|
+
return this.currentIgnoreRule.findIgnoreReason(mutatorName, line);
|
|
1146
|
+
}
|
|
1147
|
+
warnAboutUnusedDirective(mutators, directiveType, scope, comment) {
|
|
1148
|
+
for (const mutator of mutators) {
|
|
1149
|
+
if (mutator === WILDCARD) continue;
|
|
1150
|
+
if (!this.allMutatorNames.includes(mutator.toLowerCase())) {
|
|
1151
|
+
const commentLoc = comment.loc;
|
|
1152
|
+
if (commentLoc === void 0 || commentLoc === null || commentLoc.start === null || commentLoc.start === void 0) throw new Error("Comment without location");
|
|
1153
|
+
this.logger.warn(`Unused 'Stryker ${scope ? directiveType + " " + scope : directiveType}' directive. Mutator with name '${mutator}' not found. Directive found at: ${this.originFileName}:${commentLoc.start.line}:${commentLoc.start.column}.`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/transformers/ignorer-bookkeeper.ts
|
|
1160
|
+
/**
|
|
1161
|
+
* Responsible for keeping track of the active ignore message and node using the configured ignore-plugins.
|
|
1162
|
+
*/
|
|
1163
|
+
var IgnorerBookkeeper = class {
|
|
1164
|
+
ignorers;
|
|
1165
|
+
activeIgnored;
|
|
1166
|
+
get currentIgnoreMessage() {
|
|
1167
|
+
return this.activeIgnored?.message;
|
|
1168
|
+
}
|
|
1169
|
+
constructor(ignorers) {
|
|
1170
|
+
this.ignorers = ignorers;
|
|
1171
|
+
}
|
|
1172
|
+
enterNode(path) {
|
|
1173
|
+
if (!this.activeIgnored) this.ignorers.forEach((ignorer) => {
|
|
1174
|
+
const message = ignorer.shouldIgnore(path);
|
|
1175
|
+
if (message) this.activeIgnored = {
|
|
1176
|
+
node: path.node,
|
|
1177
|
+
message
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
leaveNode(path) {
|
|
1182
|
+
if (this.activeIgnored?.node === path.node) this.activeIgnored = void 0;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
//#endregion
|
|
1186
|
+
//#region src/transformers/babel-transformer.ts
|
|
1187
|
+
const { traverse: traverse$1 } = babel;
|
|
1188
|
+
const transformBabel = ({ root, originFileName, rawContent, offset }, mutantCollector, { options, mutateDescription, logger }, mutators = allMutators, mutantPlacers = allMutantPlacers) => {
|
|
1189
|
+
const file = createBabelFile(originFileName, rawContent, root);
|
|
1190
|
+
const placementMap = /* @__PURE__ */ new Map();
|
|
1191
|
+
const directiveBookkeeper = new DirectiveBookkeeper(logger, mutators, originFileName);
|
|
1192
|
+
const ignorerBookkeeper = new IgnorerBookkeeper(options.ignorers);
|
|
1193
|
+
traverse$1(file.ast, {
|
|
1194
|
+
enter(path) {
|
|
1195
|
+
directiveBookkeeper.processStrykerDirectives(path.node);
|
|
1196
|
+
if (shouldSkip(path)) path.skip();
|
|
1197
|
+
else {
|
|
1198
|
+
ignorerBookkeeper.enterNode(path);
|
|
1199
|
+
addToPlacementMapIfPossible(path);
|
|
1200
|
+
if (shouldMutate(path)) {
|
|
1201
|
+
const mutantsToPlace = collectMutants(path);
|
|
1202
|
+
if (mutantsToPlace.length) {
|
|
1203
|
+
const placementPath = path.find((ancestor) => placementMap.has(ancestor.node));
|
|
1204
|
+
if (placementPath) {
|
|
1205
|
+
const placement = placementMap.get(placementPath.node);
|
|
1206
|
+
if (placement === void 0) throw new Error("Placement not found for node");
|
|
1207
|
+
const { appliedMutants } = placement;
|
|
1208
|
+
mutantsToPlace.forEach((mutant) => appliedMutants.set(mutant, mutant.applied(placementPath.node)));
|
|
1209
|
+
} else throw new Error(`Mutants cannot be placed. This shouldn't happen! Unplaced mutants: ${JSON.stringify(mutantsToPlace, null, 2)}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
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
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Don't traverse import declarations, decorators and nodes that don't have overlap with the selected mutation ranges
|
|
1232
|
+
*/
|
|
1233
|
+
function shouldSkip(path) {
|
|
1234
|
+
return isTypeNode(path) || isImportDeclaration(path) || path.isDecorator() || !mutateDescription || Array.isArray(mutateDescription) && mutateDescription.every((range) => !locationOverlaps(range, getNodeLocation(path.node)));
|
|
1235
|
+
}
|
|
1236
|
+
function shouldMutate(path) {
|
|
1237
|
+
return mutateDescription === true || Array.isArray(mutateDescription) && mutateDescription.some((range) => locationIncluded(range, getNodeLocation(path.node)));
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Place mutants that are assigned to the current node path (on exit)
|
|
1241
|
+
*/
|
|
1242
|
+
function placeMutantsIfNeeded(path) {
|
|
1243
|
+
const mutantsPlacement = placementMap.get(path.node);
|
|
1244
|
+
if (mutantsPlacement?.appliedMutants.size) try {
|
|
1245
|
+
mutantsPlacement.placer.place(path, mutantsPlacement.appliedMutants);
|
|
1246
|
+
path.skip();
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
throwPlacementError(toError(error), path, mutantsPlacement.placer, [...mutantsPlacement.appliedMutants.keys()], originFileName);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Collect the mutants for the current node and return the non-ignored.
|
|
1253
|
+
*/
|
|
1254
|
+
function collectMutants(path) {
|
|
1255
|
+
return [...mutate(path)].map((mutable) => mutantCollector.collect(originFileName, path.node, mutable, offset)).filter((mutant) => !mutant.ignoreReason);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Generate mutants for the current node.
|
|
1259
|
+
* @yields {Mutable} A mutable describing the mutant to be placed
|
|
1260
|
+
*/
|
|
1261
|
+
function* mutate(node) {
|
|
1262
|
+
for (const mutator of mutators) for (const replacement of mutator.mutate(node)) {
|
|
1263
|
+
const ignoreReason = directiveBookkeeper.findIgnoreReason(getNodeLocation(node.node).start.line, mutator.name) ?? findExcludedMutatorIgnoreReason(mutator.name) ?? ignorerBookkeeper.currentIgnoreMessage;
|
|
1264
|
+
yield {
|
|
1265
|
+
replacement,
|
|
1266
|
+
mutatorName: mutator.name,
|
|
1267
|
+
...ignoreReason === void 0 ? {} : { ignoreReason }
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function findExcludedMutatorIgnoreReason(mutatorName) {
|
|
1271
|
+
if (options.excludedMutations.includes(mutatorName)) return `Ignored because of excluded mutation "${mutatorName}"`;
|
|
1272
|
+
else return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
function getNodeLocation(node) {
|
|
1277
|
+
const loc = node.loc;
|
|
1278
|
+
if (loc === void 0 || loc === null || loc.start === null || loc.start === void 0 || loc.end === null || loc.end === void 0) throw new Error("Babel node without location");
|
|
1279
|
+
return loc;
|
|
1280
|
+
}
|
|
1281
|
+
function toError(value) {
|
|
1282
|
+
if (value instanceof Error) return value;
|
|
1283
|
+
return new Error("Unexpected error", { cause: value });
|
|
1284
|
+
}
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/transformers/html-transformer.ts
|
|
1287
|
+
const transformHtml = ({ root }, mutantCollector, context) => {
|
|
1288
|
+
root.scripts.forEach((ast) => {
|
|
1289
|
+
context.transform(ast, mutantCollector, context);
|
|
1290
|
+
});
|
|
1291
|
+
};
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region src/transformers/svelte-transformer.ts
|
|
1294
|
+
const moduleScript = `<script context="module">
|
|
1295
|
+
\n<\/script>\n`;
|
|
1296
|
+
const transformSvelte = (svelte, mutantCollector, context) => {
|
|
1297
|
+
const { root, originFileName } = svelte;
|
|
1298
|
+
[root.moduleScript, ...root.additionalScripts].filter(notEmpty).forEach((script) => {
|
|
1299
|
+
context.transform(script.ast, mutantCollector, {
|
|
1300
|
+
...context,
|
|
1301
|
+
options: {
|
|
1302
|
+
...context.options,
|
|
1303
|
+
noHeader: true
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
if (mutantCollector.hasPlacedMutants(originFileName)) {
|
|
1308
|
+
if (!root.moduleScript) {
|
|
1309
|
+
root.moduleScript = {
|
|
1310
|
+
ast: {
|
|
1311
|
+
format: "js",
|
|
1312
|
+
root: types.file(types.program([])),
|
|
1313
|
+
rawContent: "",
|
|
1314
|
+
originFileName
|
|
1315
|
+
},
|
|
1316
|
+
range: {
|
|
1317
|
+
start: 26,
|
|
1318
|
+
end: 26
|
|
1319
|
+
},
|
|
1320
|
+
isExpression: false
|
|
1321
|
+
};
|
|
1322
|
+
svelte.rawContent = `${moduleScript}${svelte.rawContent}`;
|
|
1323
|
+
svelte.root.additionalScripts.forEach((script) => {
|
|
1324
|
+
script.range.start += moduleScript.length;
|
|
1325
|
+
script.range.end += moduleScript.length;
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
placeHeader(root.moduleScript.ast.root);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
//#endregion
|
|
1332
|
+
//#region src/transformers/transformer.ts
|
|
1333
|
+
/**
|
|
1334
|
+
* Transform the AST by generating mutants and placing them in the AST.
|
|
1335
|
+
* Supports all AST formats supported by Stryker.
|
|
1336
|
+
* @param ast The Abstract Syntax Tree
|
|
1337
|
+
* @param mutantCollector the mutant collector that will be used to register and administer mutants
|
|
1338
|
+
* @param transformerContext the options used during transforming
|
|
1339
|
+
*/
|
|
1340
|
+
function transform(ast, mutantCollector, transformerContext) {
|
|
1341
|
+
const context = {
|
|
1342
|
+
...transformerContext,
|
|
1343
|
+
transform
|
|
1344
|
+
};
|
|
1345
|
+
switch (ast.format) {
|
|
1346
|
+
case "html":
|
|
1347
|
+
transformHtml(ast, mutantCollector, context);
|
|
1348
|
+
break;
|
|
1349
|
+
case "js":
|
|
1350
|
+
case "ts":
|
|
1351
|
+
case "tsx":
|
|
1352
|
+
transformBabel(ast, mutantCollector, context);
|
|
1353
|
+
break;
|
|
1354
|
+
case "svelte": transformSvelte(ast, mutantCollector, context);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region src/util/syntax-helpers.ts
|
|
1359
|
+
const STRYKER_NAMESPACE_HELPER = "stryNS_9fa48";
|
|
1360
|
+
const COVER_MUTANT_HELPER = "stryCov_9fa48";
|
|
1361
|
+
const IS_MUTANT_ACTIVE_HELPER = "stryMutAct_9fa48";
|
|
1362
|
+
const { types: types$2, traverse } = babel;
|
|
1363
|
+
/**
|
|
1364
|
+
* Returns syntax for the header if JS/TS files
|
|
1365
|
+
*/
|
|
1366
|
+
const parsedInstrumentationHeader = babel.parse(`function ${STRYKER_NAMESPACE_HELPER}(){
|
|
1367
|
+
var g = typeof globalThis === 'object' && globalThis && globalThis.Math === Math && globalThis || new Function("return this")();
|
|
1368
|
+
var ns = g.${ID.NAMESPACE} || (g.${ID.NAMESPACE} = {});
|
|
1369
|
+
if (ns.${ID.ACTIVE_MUTANT} === undefined && g.process && g.process.env && g.process.env.${ID.ACTIVE_MUTANT_ENV_VARIABLE}) {
|
|
1370
|
+
ns.${ID.ACTIVE_MUTANT} = g.process.env.${ID.ACTIVE_MUTANT_ENV_VARIABLE};
|
|
1371
|
+
}
|
|
1372
|
+
function retrieveNS(){
|
|
1373
|
+
return ns;
|
|
1374
|
+
}
|
|
1375
|
+
${STRYKER_NAMESPACE_HELPER} = retrieveNS;
|
|
1376
|
+
return retrieveNS();
|
|
1377
|
+
}
|
|
1378
|
+
${STRYKER_NAMESPACE_HELPER}();
|
|
1379
|
+
|
|
1380
|
+
function ${COVER_MUTANT_HELPER}() {
|
|
1381
|
+
var ns = ${STRYKER_NAMESPACE_HELPER}();
|
|
1382
|
+
var cov = ns.${ID.MUTATION_COVERAGE_OBJECT} || (ns.${ID.MUTATION_COVERAGE_OBJECT} = { static: {}, perTest: {} });
|
|
1383
|
+
function cover() {
|
|
1384
|
+
var c = cov.static;
|
|
1385
|
+
if (ns.${ID.CURRENT_TEST_ID}) {
|
|
1386
|
+
c = cov.perTest[ns.${ID.CURRENT_TEST_ID}] = cov.perTest[ns.${ID.CURRENT_TEST_ID}] || {};
|
|
1387
|
+
}
|
|
1388
|
+
var a = arguments;
|
|
1389
|
+
for(var i=0; i < a.length; i++){
|
|
1390
|
+
c[a[i]] = (c[a[i]] || 0) + 1;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
${COVER_MUTANT_HELPER} = cover;
|
|
1394
|
+
cover.apply(null, arguments);
|
|
1395
|
+
}
|
|
1396
|
+
function ${IS_MUTANT_ACTIVE_HELPER}(id) {
|
|
1397
|
+
var ns = ${STRYKER_NAMESPACE_HELPER}();
|
|
1398
|
+
function isActive(id) {
|
|
1399
|
+
if (ns.${ID.ACTIVE_MUTANT} === id) {
|
|
1400
|
+
if (ns.${ID.HIT_COUNT} !== void 0 && ++ns.${ID.HIT_COUNT} > ns.${ID.HIT_LIMIT}) {
|
|
1401
|
+
throw new Error('Stryker: Hit count limit reached (' + ns.${ID.HIT_COUNT} + ')');
|
|
1402
|
+
}
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
${IS_MUTANT_ACTIVE_HELPER} = isActive;
|
|
1408
|
+
return isActive(id);
|
|
1409
|
+
}`, {
|
|
1410
|
+
configFile: false,
|
|
1411
|
+
browserslistConfigFile: false,
|
|
1412
|
+
env: { targets: {} }
|
|
1413
|
+
});
|
|
1414
|
+
if (!types$2.isFile(parsedInstrumentationHeader)) throw new Error("Instrumentation header parsed as non-File");
|
|
1415
|
+
const instrumentationBabelHeader = parsedInstrumentationHeader.program.body;
|
|
1416
|
+
deepFreeze(instrumentationBabelHeader);
|
|
1417
|
+
/**
|
|
1418
|
+
* returns syntax for `global.activeMutant === $mutantId`
|
|
1419
|
+
* @param mutantId The id of the mutant to switch
|
|
1420
|
+
*/
|
|
1421
|
+
function mutantTestExpression(mutantId) {
|
|
1422
|
+
return types$2.callExpression(types$2.identifier(IS_MUTANT_ACTIVE_HELPER), [types$2.stringLiteral(mutantId)]);
|
|
1423
|
+
}
|
|
1424
|
+
function eqLocation(a, b) {
|
|
1425
|
+
function eqPosition(start, end) {
|
|
1426
|
+
return start.column === end.column && start.line === end.line;
|
|
1427
|
+
}
|
|
1428
|
+
return eqPosition(a.start, b.start) && eqPosition(a.end, b.end);
|
|
1429
|
+
}
|
|
1430
|
+
function eqNode(a, b) {
|
|
1431
|
+
return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Returns a sequence of mutation coverage counters with an optional last expression.
|
|
1435
|
+
*
|
|
1436
|
+
* @example (global.__coverMutant__(0, 1), 40 + 2)
|
|
1437
|
+
* @param mutants The mutants for which covering syntax needs to be generated
|
|
1438
|
+
* @param targetExpression The original expression
|
|
1439
|
+
*/
|
|
1440
|
+
function mutationCoverageSequenceExpression(mutants, targetExpression) {
|
|
1441
|
+
const mutantIds = [...mutants].map((mutant) => types$2.stringLiteral(mutant.id));
|
|
1442
|
+
const sequence = [types$2.callExpression(types$2.identifier(COVER_MUTANT_HELPER), mutantIds)];
|
|
1443
|
+
if (targetExpression) sequence.push(targetExpression);
|
|
1444
|
+
return types$2.sequenceExpression(sequence);
|
|
1445
|
+
}
|
|
1446
|
+
function isTypeNode(path) {
|
|
1447
|
+
return path.isTypeAnnotation() || flowTypeAnnotationNodeTypes.includes(path.node.type) || tsTypeAnnotationNodeTypes.includes(path.node.type) || isDeclareVariableStatement(path) || isDeclareModule(path);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Determines whether or not it is a declare variable statement node.
|
|
1451
|
+
* @example
|
|
1452
|
+
* declare const foo: 'foo';
|
|
1453
|
+
*/
|
|
1454
|
+
function isDeclareVariableStatement(path) {
|
|
1455
|
+
return path.isVariableDeclaration() && path.node.declare === true;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Determines whether or not a node is a string literal that is the name of a module.
|
|
1459
|
+
* @example
|
|
1460
|
+
* declare module "express" {};
|
|
1461
|
+
*/
|
|
1462
|
+
function isDeclareModule(path) {
|
|
1463
|
+
return path.isTSModuleDeclaration() && (path.node.declare ?? false);
|
|
1464
|
+
}
|
|
1465
|
+
const tsTypeAnnotationNodeTypes = Object.freeze([
|
|
1466
|
+
"TSAsExpression",
|
|
1467
|
+
"TSInterfaceDeclaration",
|
|
1468
|
+
"TSTypeAnnotation",
|
|
1469
|
+
"TSTypeAliasDeclaration",
|
|
1470
|
+
"TSEnumDeclaration",
|
|
1471
|
+
"TSDeclareFunction",
|
|
1472
|
+
"TSTypeParameterInstantiation",
|
|
1473
|
+
"TSTypeParameterDeclaration"
|
|
1474
|
+
]);
|
|
1475
|
+
const flowTypeAnnotationNodeTypes = Object.freeze([
|
|
1476
|
+
"DeclareClass",
|
|
1477
|
+
"DeclareFunction",
|
|
1478
|
+
"DeclareInterface",
|
|
1479
|
+
"DeclareModule",
|
|
1480
|
+
"DeclareModuleExports",
|
|
1481
|
+
"DeclareTypeAlias",
|
|
1482
|
+
"DeclareOpaqueType",
|
|
1483
|
+
"DeclareVariable",
|
|
1484
|
+
"DeclareExportDeclaration",
|
|
1485
|
+
"DeclareExportAllDeclaration",
|
|
1486
|
+
"InterfaceDeclaration",
|
|
1487
|
+
"OpaqueType",
|
|
1488
|
+
"TypeAlias",
|
|
1489
|
+
"InterfaceDeclaration"
|
|
1490
|
+
]);
|
|
1491
|
+
function isImportDeclaration(path) {
|
|
1492
|
+
return types$2.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration();
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Determines if a location (needle) is included in an other location (haystack)
|
|
1496
|
+
* @param haystack The range to look in
|
|
1497
|
+
* @param needle the range to search for
|
|
1498
|
+
*/
|
|
1499
|
+
function locationIncluded(haystack, needle) {
|
|
1500
|
+
const startIncluded = haystack.start.line < needle.start.line || haystack.start.line === needle.start.line && haystack.start.column <= needle.start.column;
|
|
1501
|
+
const endIncluded = haystack.end.line > needle.end.line || haystack.end.line === needle.end.line && haystack.end.column >= needle.end.column;
|
|
1502
|
+
return startIncluded && endIncluded;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Determines if two locations overlap with each other
|
|
1506
|
+
*/
|
|
1507
|
+
function locationOverlaps(a, b) {
|
|
1508
|
+
const startIncluded = a.start.line < b.end.line || a.start.line === b.end.line && a.start.column <= b.end.column;
|
|
1509
|
+
const endIncluded = a.end.line > b.start.line || a.end.line === b.start.line && a.end.column >= b.start.column;
|
|
1510
|
+
return startIncluded && endIncluded;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Helper for `types.cloneNode(node, deep: true, withoutLocations: false);`
|
|
1514
|
+
*/
|
|
1515
|
+
function deepCloneNode(node) {
|
|
1516
|
+
return types$2.cloneNode(node, true, false);
|
|
1517
|
+
}
|
|
1518
|
+
function placeHeaderIfNeeded(mutantCollector, originFileName, options, root) {
|
|
1519
|
+
if (mutantCollector.hasPlacedMutants(originFileName) && !options.noHeader) placeHeader(root);
|
|
1520
|
+
}
|
|
1521
|
+
function placeHeader(root) {
|
|
1522
|
+
let header = instrumentationBabelHeader;
|
|
1523
|
+
const leadingComments = root.program.body[0]?.leadingComments;
|
|
1524
|
+
if (Array.isArray(leadingComments)) {
|
|
1525
|
+
const firstHeader = instrumentationBabelHeader[0];
|
|
1526
|
+
if (firstHeader === void 0) throw new Error("Instrumentation header is empty");
|
|
1527
|
+
const cloned = types$2.cloneNode(firstHeader, true, false);
|
|
1528
|
+
cloned.leadingComments = leadingComments;
|
|
1529
|
+
header = [cloned, ...instrumentationBabelHeader.slice(1)];
|
|
1530
|
+
}
|
|
1531
|
+
root.program.body.unshift(...header);
|
|
1532
|
+
}
|
|
1533
|
+
//#endregion
|
|
1534
|
+
//#region src/parsers/svelte-parser.ts
|
|
1535
|
+
const MIN_SVELTE_VERSION = ">=3.30";
|
|
1536
|
+
function isPlainRecord(value) {
|
|
1537
|
+
return typeof value === "object" && value !== null;
|
|
1538
|
+
}
|
|
1539
|
+
function isUnknownArray(value) {
|
|
1540
|
+
return Array.isArray(value);
|
|
1541
|
+
}
|
|
1542
|
+
function isRangedProgram(value) {
|
|
1543
|
+
return isPlainRecord(value) && typeof value["start"] === "number" && typeof value["end"] === "number";
|
|
1544
|
+
}
|
|
1545
|
+
function isWalkFunction(value) {
|
|
1546
|
+
return typeof value === "function";
|
|
1547
|
+
}
|
|
1548
|
+
function isRangedBaseNode(value) {
|
|
1549
|
+
return isPlainRecord(value) && typeof value["type"] === "string" && typeof value["start"] === "number" && typeof value["end"] === "number";
|
|
1550
|
+
}
|
|
1551
|
+
function isTemplateExpressionType(type) {
|
|
1552
|
+
return type === "MustacheTag" || type === "RawMustacheTag" || type === "IfBlock" || type === "ConstTag" || type === "EachBlock" || type === "AwaitBlock" || type === "KeyBlock" || type === "EventHandler";
|
|
1553
|
+
}
|
|
1554
|
+
function tryGetScriptRangeFromElement(node) {
|
|
1555
|
+
if (!isPlainRecord(node) || node["type"] !== "Element" || node["name"] !== "script") return;
|
|
1556
|
+
const children = node["children"];
|
|
1557
|
+
if (!isUnknownArray(children) || children.length === 0) return;
|
|
1558
|
+
const firstChild = children[0];
|
|
1559
|
+
if (!isPlainRecord(firstChild) || firstChild["type"] !== "Text" || typeof firstChild["start"] !== "number" || typeof firstChild["end"] !== "number") return;
|
|
1560
|
+
return {
|
|
1561
|
+
start: firstChild["start"],
|
|
1562
|
+
end: firstChild["end"],
|
|
1563
|
+
isExpression: false
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async function parse$1(text, fileName, context) {
|
|
1567
|
+
const { parse: svelteParse, preprocess, VERSION } = await import("./compiler-Ck-td5Ds.mjs");
|
|
1568
|
+
let walk;
|
|
1569
|
+
if (!satisfies(VERSION, MIN_SVELTE_VERSION)) throw new Error(`Svelte version ${VERSION} not supported. Expected: ${MIN_SVELTE_VERSION} (processing file ${fileName})`);
|
|
1570
|
+
if (satisfies(VERSION, ">=5")) {
|
|
1571
|
+
const walkerModule = await import(import.meta.resolve("estree-walker", import.meta.resolve("svelte")));
|
|
1572
|
+
if (!isPlainRecord(walkerModule) || !isWalkFunction(walkerModule["walk"])) throw new Error("estree-walker module without walk export");
|
|
1573
|
+
walk = walkerModule["walk"];
|
|
1574
|
+
} else {
|
|
1575
|
+
const svelteCompilerModule = await import("./compiler-Ck-td5Ds.mjs");
|
|
1576
|
+
if (!isPlainRecord(svelteCompilerModule) || !isWalkFunction(svelteCompilerModule["walk"])) throw new Error("svelte/compiler module without walk export");
|
|
1577
|
+
walk = svelteCompilerModule["walk"];
|
|
1578
|
+
}
|
|
1579
|
+
const positionConverter = new PositionConverter(text);
|
|
1580
|
+
const { replacedCode, scriptMap } = await replaceScripts(text);
|
|
1581
|
+
const svelteAst = svelteParse(replacedCode, { filename: fileName });
|
|
1582
|
+
const { remappedModuleScriptRange, remappedScriptRanges } = remapScriptLocations(replacedCode, scriptMap, getModuleScriptRange(svelteAst), getTemplateScriptRanges(svelteAst, walk));
|
|
1583
|
+
const [moduleScript, ...additionalScripts] = await Promise.all([parseTemplateScriptIfDefined(remappedModuleScriptRange), ...remappedScriptRanges.map(parseTemplateScript)]);
|
|
1584
|
+
return {
|
|
1585
|
+
originFileName: fileName,
|
|
1586
|
+
rawContent: text,
|
|
1587
|
+
format: "svelte",
|
|
1588
|
+
root: {
|
|
1589
|
+
...moduleScript === void 0 ? {} : { moduleScript },
|
|
1590
|
+
additionalScripts
|
|
1591
|
+
}
|
|
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
|
+
}
|
|
1609
|
+
function getTemplateScriptRanges(ast, walker) {
|
|
1610
|
+
const ranges = [];
|
|
1611
|
+
if (isPlainRecord(ast) && ast["instance"] !== null && ast["instance"] !== void 0) {
|
|
1612
|
+
const instance = ast["instance"];
|
|
1613
|
+
if (isPlainRecord(instance) && "content" in instance) {
|
|
1614
|
+
const content = instance["content"];
|
|
1615
|
+
if (isRangedProgram(content)) ranges.push({
|
|
1616
|
+
start: content.start,
|
|
1617
|
+
end: content.end,
|
|
1618
|
+
isExpression: false
|
|
1619
|
+
});
|
|
1620
|
+
else throw new Error("Svelte instance script without a source range");
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (!isPlainRecord(ast) || !("html" in ast)) throw new Error("Svelte AST without html");
|
|
1624
|
+
const html = ast["html"];
|
|
1625
|
+
walker(html, { enter(node) {
|
|
1626
|
+
const scriptRange = tryGetScriptRangeFromElement(node);
|
|
1627
|
+
if (scriptRange) ranges.push(scriptRange);
|
|
1628
|
+
const templateExpression = collectTemplateExpression(node);
|
|
1629
|
+
if (templateExpression) {
|
|
1630
|
+
const { start, end } = templateExpression;
|
|
1631
|
+
ranges.push({
|
|
1632
|
+
start,
|
|
1633
|
+
end,
|
|
1634
|
+
isExpression: true
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
} });
|
|
1638
|
+
return ranges;
|
|
1639
|
+
}
|
|
1640
|
+
async function parseTemplateScriptIfDefined(range) {
|
|
1641
|
+
if (range) return parseTemplateScript(range);
|
|
1642
|
+
}
|
|
1643
|
+
async function parseTemplateScript({ start, end, isExpression, format }) {
|
|
1644
|
+
const scriptText = text.slice(start, end);
|
|
1645
|
+
return {
|
|
1646
|
+
ast: {
|
|
1647
|
+
...await context.parse(scriptText, fileName, format),
|
|
1648
|
+
offset: positionConverter.positionFromOffset(start)
|
|
1649
|
+
},
|
|
1650
|
+
range: {
|
|
1651
|
+
start,
|
|
1652
|
+
end
|
|
1653
|
+
},
|
|
1654
|
+
isExpression
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function getModuleScriptRange(svelteAst) {
|
|
1659
|
+
if (!isPlainRecord(svelteAst) || !("module" in svelteAst) || svelteAst["module"] === null || svelteAst["module"] === void 0) return;
|
|
1660
|
+
const mod = svelteAst["module"];
|
|
1661
|
+
if (!isPlainRecord(mod) || !("content" in mod)) throw new Error("Svelte module script without a source range");
|
|
1662
|
+
const content = mod["content"];
|
|
1663
|
+
if (!isRangedProgram(content)) throw new Error("Svelte module script without a source range");
|
|
1664
|
+
return {
|
|
1665
|
+
start: content.start,
|
|
1666
|
+
end: content.end,
|
|
1667
|
+
isExpression: false
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Remaps script locations back to the original places using the script map
|
|
1672
|
+
*/
|
|
1673
|
+
function remapScriptLocations(code, scriptMap, moduleScriptRange, templateRanges) {
|
|
1674
|
+
const scriptRanges = [moduleScriptRange, ...templateRanges].filter(notEmpty).sort((a, b) => a.start - b.start);
|
|
1675
|
+
let offset = 0;
|
|
1676
|
+
let newModuleScriptRange;
|
|
1677
|
+
const newScriptRanges = scriptRanges.map((range) => {
|
|
1678
|
+
const script = code.substring(range.start, range.end);
|
|
1679
|
+
const actualScript = scriptMap.get(script);
|
|
1680
|
+
const start = range.start + offset;
|
|
1681
|
+
if (actualScript) {
|
|
1682
|
+
const scriptRange = {
|
|
1683
|
+
start,
|
|
1684
|
+
end: start + actualScript.content.length,
|
|
1685
|
+
isExpression: range.isExpression,
|
|
1686
|
+
format: actualScript.attributes["lang"] === "ts" ? "ts" : "js"
|
|
1687
|
+
};
|
|
1688
|
+
offset += actualScript.content.length - script.length;
|
|
1689
|
+
if (range === moduleScriptRange) newModuleScriptRange = scriptRange;
|
|
1690
|
+
return scriptRange;
|
|
1691
|
+
} else return {
|
|
1692
|
+
start,
|
|
1693
|
+
end: start + script.length,
|
|
1694
|
+
isExpression: range.isExpression,
|
|
1695
|
+
format: "js"
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1698
|
+
return {
|
|
1699
|
+
remappedModuleScriptRange: newModuleScriptRange,
|
|
1700
|
+
remappedScriptRanges: newScriptRanges.filter((range) => range !== newModuleScriptRange)
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function collectTemplateExpression(node) {
|
|
1704
|
+
if (!isPlainRecord(node) || typeof node["type"] !== "string") return;
|
|
1705
|
+
if (!isTemplateExpressionType(node["type"])) return;
|
|
1706
|
+
if (!("expression" in node)) return;
|
|
1707
|
+
const expression = node["expression"];
|
|
1708
|
+
if (isRangedBaseNode(expression)) return expression;
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region src/parsers/ts-parser.ts
|
|
1712
|
+
const { types: types$1, parseAsync } = babel;
|
|
1713
|
+
const require = createRequire(import.meta.url);
|
|
1714
|
+
/**
|
|
1715
|
+
* See https://babeljs.io/docs/en/babel-preset-typescript
|
|
1716
|
+
* @param text The text to parse
|
|
1717
|
+
* @param fileName The name of the file
|
|
1718
|
+
*/
|
|
1719
|
+
async function parseTS(text, fileName) {
|
|
1720
|
+
return {
|
|
1721
|
+
originFileName: fileName,
|
|
1722
|
+
rawContent: text,
|
|
1723
|
+
format: "ts",
|
|
1724
|
+
root: await parse(text, fileName, false)
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
async function parseTsx(text, fileName) {
|
|
1728
|
+
return {
|
|
1729
|
+
root: await parse(text, fileName, true),
|
|
1730
|
+
format: "tsx",
|
|
1731
|
+
originFileName: fileName,
|
|
1732
|
+
rawContent: text
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
async function parse(text, fileName, isTSX) {
|
|
1736
|
+
const ast = await parseAsync(text, {
|
|
1737
|
+
filename: fileName,
|
|
1738
|
+
parserOpts: { ranges: true },
|
|
1739
|
+
configFile: false,
|
|
1740
|
+
babelrc: false,
|
|
1741
|
+
presets: [[require.resolve("@babel/preset-typescript"), {
|
|
1742
|
+
isTSX,
|
|
1743
|
+
allExtensions: true
|
|
1744
|
+
}]],
|
|
1745
|
+
plugins: [[require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], [require.resolve("@babel/plugin-transform-explicit-resource-management")]]
|
|
1746
|
+
});
|
|
1747
|
+
if (ast === null) throw new Error(`Expected ${fileName} to contain a babel.types.file, but it yielded null`);
|
|
1748
|
+
if (types$1.isProgram(ast)) throw new Error(`Expected ${fileName} to contain a babel.types.file, but was a program`);
|
|
1749
|
+
return ast;
|
|
1750
|
+
}
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/parsers/create-parser.ts
|
|
1753
|
+
function createParser(parserOptions) {
|
|
1754
|
+
const jsParse = createParser$1(parserOptions);
|
|
1755
|
+
async function parse(code, fileName, formatOverride) {
|
|
1756
|
+
const format = getFormat(fileName, formatOverride);
|
|
1757
|
+
if (!format) {
|
|
1758
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
1759
|
+
throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
|
|
1760
|
+
}
|
|
1761
|
+
switch (format) {
|
|
1762
|
+
case "js": return jsParse(code, fileName);
|
|
1763
|
+
case "tsx": return parseTsx(code, fileName);
|
|
1764
|
+
case "ts": return parseTS(code, fileName);
|
|
1765
|
+
case "html": return parse$2(code, fileName, { parse });
|
|
1766
|
+
case "svelte": return parse$1(code, fileName, { parse });
|
|
1767
|
+
default: throw new Error(`Unsupported format: ${String(format)}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return parse;
|
|
1771
|
+
}
|
|
1772
|
+
function getFormat(fileName, override) {
|
|
1773
|
+
if (override) return override;
|
|
1774
|
+
else switch (path.extname(fileName).toLowerCase()) {
|
|
1775
|
+
case ".js":
|
|
1776
|
+
case ".jsx":
|
|
1777
|
+
case ".mjs":
|
|
1778
|
+
case ".cjs": return "js";
|
|
1779
|
+
case ".mts":
|
|
1780
|
+
case ".cts":
|
|
1781
|
+
case ".ts": return "ts";
|
|
1782
|
+
case ".tsx": return "tsx";
|
|
1783
|
+
case ".vue":
|
|
1784
|
+
case ".html":
|
|
1785
|
+
case ".htm": return "html";
|
|
1786
|
+
case ".svelte": return "svelte";
|
|
1787
|
+
default: return;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
//#endregion
|
|
1791
|
+
//#region src/printers/html-printer.ts
|
|
1792
|
+
function getScriptStart$1(script) {
|
|
1793
|
+
const start = script.root.start;
|
|
1794
|
+
if (start === void 0 || start === null) throw new Error("Script AST root without start");
|
|
1795
|
+
return start;
|
|
1796
|
+
}
|
|
1797
|
+
function getScriptEnd$1(script) {
|
|
1798
|
+
const end = script.root.end;
|
|
1799
|
+
if (end === void 0 || end === null) throw new Error("Script AST root without end");
|
|
1800
|
+
return end;
|
|
1801
|
+
}
|
|
1802
|
+
const print$4 = (ast, context) => {
|
|
1803
|
+
const sortedScripts = [...ast.root.scripts].sort((a, b) => getScriptStart$1(a) - getScriptStart$1(b));
|
|
1804
|
+
let currentIndex = 0;
|
|
1805
|
+
let html = "";
|
|
1806
|
+
for (const script of sortedScripts) {
|
|
1807
|
+
html += ast.rawContent.substring(currentIndex, getScriptStart$1(script));
|
|
1808
|
+
html += "\n";
|
|
1809
|
+
html += context.print(script, context);
|
|
1810
|
+
html += "\n";
|
|
1811
|
+
currentIndex = getScriptEnd$1(script);
|
|
1812
|
+
}
|
|
1813
|
+
html += ast.rawContent.substr(currentIndex);
|
|
1814
|
+
return html;
|
|
1815
|
+
};
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region src/printers/js-printer.ts
|
|
1818
|
+
const print$3 = (file) => {
|
|
1819
|
+
return generate(file.root, { sourceMaps: false }).code;
|
|
1820
|
+
};
|
|
1821
|
+
//#endregion
|
|
1822
|
+
//#region src/printers/svelte-printer.ts
|
|
1823
|
+
const print$2 = ({ root, rawContent }, context) => {
|
|
1824
|
+
let currentIndex = 0;
|
|
1825
|
+
let outputText = "";
|
|
1826
|
+
const sortedScripts = [root.moduleScript, ...root.additionalScripts].filter(notEmpty).sort((a, b) => a.range.start - b.range.start);
|
|
1827
|
+
for (const script of sortedScripts) if (script.isExpression) {
|
|
1828
|
+
const codeWithoutSemicolon = context.print(script.ast, context).slice(0, -1);
|
|
1829
|
+
outputText += rawContent.substring(currentIndex, script.range.start) + codeWithoutSemicolon;
|
|
1830
|
+
currentIndex = script.range.end;
|
|
1831
|
+
} else {
|
|
1832
|
+
outputText += rawContent.substring(currentIndex, script.range.start);
|
|
1833
|
+
outputText += "\n";
|
|
1834
|
+
outputText += context.print(script.ast, context);
|
|
1835
|
+
outputText += "\n";
|
|
1836
|
+
currentIndex = script.range.end;
|
|
1837
|
+
}
|
|
1838
|
+
outputText += rawContent.substring(currentIndex);
|
|
1839
|
+
return outputText;
|
|
1840
|
+
};
|
|
1841
|
+
//#endregion
|
|
1842
|
+
//#region src/printers/ts-printer.ts
|
|
1843
|
+
const print$1 = (file) => {
|
|
1844
|
+
return generate(file.root, {
|
|
1845
|
+
decoratorsBeforeExport: true,
|
|
1846
|
+
sourceMaps: false
|
|
1847
|
+
}).code;
|
|
1848
|
+
};
|
|
1849
|
+
//#endregion
|
|
1850
|
+
//#region src/printers/index.ts
|
|
1851
|
+
function print(file) {
|
|
1852
|
+
const context = { print };
|
|
1853
|
+
switch (file.format) {
|
|
1854
|
+
case "js": return print$3(file, context);
|
|
1855
|
+
case "ts": return print$1(file, context);
|
|
1856
|
+
case "tsx": return print$1(file, context);
|
|
1857
|
+
case "html": return print$4(file, context);
|
|
1858
|
+
case "svelte": return print$2(file, context);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
//#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
|
+
//#region src/instrumenter.ts
|
|
2048
|
+
/**
|
|
2049
|
+
* The instrumenter is responsible for
|
|
2050
|
+
* * Generating mutants based on source files
|
|
2051
|
+
* * Instrumenting the source code with the mutants placed in `mutant switches`.
|
|
2052
|
+
* * Adding mutant coverage expressions in the source code.
|
|
2053
|
+
* @see https://github.com/stryker-mutator/stryker-js/issues/1514
|
|
2054
|
+
*/
|
|
2055
|
+
var Instrumenter = class {
|
|
2056
|
+
logger;
|
|
2057
|
+
_createParser;
|
|
2058
|
+
_print;
|
|
2059
|
+
_transform;
|
|
2060
|
+
static inject = tokens(commonTokens.logger, instrumenterTokens.createParser, instrumenterTokens.print, instrumenterTokens.transform);
|
|
2061
|
+
constructor(logger, _createParser = createParser, _print = print, _transform = transform) {
|
|
2062
|
+
this.logger = logger;
|
|
2063
|
+
this._createParser = _createParser;
|
|
2064
|
+
this._print = _print;
|
|
2065
|
+
this._transform = _transform;
|
|
2066
|
+
}
|
|
2067
|
+
async instrument(files, options) {
|
|
2068
|
+
this.logger.debug("Instrumenting %d source files with mutants", files.length);
|
|
2069
|
+
const mutantCollector = new MutantCollector();
|
|
2070
|
+
const outFiles = [];
|
|
2071
|
+
let mutantCount = 0;
|
|
2072
|
+
const parse = this._createParser(options);
|
|
2073
|
+
for (const { name, mutate, content } of files) {
|
|
2074
|
+
const ast = await parse(content, name);
|
|
2075
|
+
this._transform(ast, mutantCollector, {
|
|
2076
|
+
options,
|
|
2077
|
+
mutateDescription: toBabelLineNumber(mutate),
|
|
2078
|
+
logger: this.logger
|
|
2079
|
+
});
|
|
2080
|
+
const mutatedContent = this._print(ast);
|
|
2081
|
+
outFiles.push({
|
|
2082
|
+
name,
|
|
2083
|
+
mutate,
|
|
2084
|
+
content: mutatedContent
|
|
2085
|
+
});
|
|
2086
|
+
if (this.logger.isDebugEnabled()) {
|
|
2087
|
+
const nrOfMutantsInFile = mutantCollector.mutants.length - mutantCount;
|
|
2088
|
+
mutantCount = mutantCollector.mutants.length;
|
|
2089
|
+
this.logger.debug(`Instrumented ${path.relative(process.cwd(), name)} (${nrOfMutantsInFile} mutant(s))`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const mutants = mutantCollector.mutants.map((mutant) => mutant.toApiMutant());
|
|
2093
|
+
this.logger.info("Instrumented %d source file(s) with %d mutant(s)", files.length, mutants.length);
|
|
2094
|
+
return {
|
|
2095
|
+
files: outFiles,
|
|
2096
|
+
mutants
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
function toBabelLineNumber(range) {
|
|
2101
|
+
if (typeof range === "boolean") return range;
|
|
2102
|
+
else return range.map(({ start, end }) => ({
|
|
2103
|
+
start: {
|
|
2104
|
+
column: start.column,
|
|
2105
|
+
line: start.line + 1
|
|
2106
|
+
},
|
|
2107
|
+
end: {
|
|
2108
|
+
column: end.column,
|
|
2109
|
+
line: end.line + 1
|
|
2110
|
+
}
|
|
2111
|
+
}));
|
|
2112
|
+
}
|
|
2113
|
+
//#endregion
|
|
2114
|
+
export { Instrumenter, createInstrumenter, disableTypeChecks, frameworkPluginsFileUrl, strykerPlugins };
|