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