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