@abloh/core 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +41404 -2367
- package/dist/index.js +35724 -3455
- package/dist/playground-admission.d.ts +142 -0
- package/dist/playground-admission.js +166 -0
- package/dist/playground-ingress.d.ts +18 -0
- package/dist/playground-ingress.js +62 -0
- package/dist/source-analysis/index.d.ts +714 -0
- package/dist/source-analysis/index.js +1945 -0
- package/package.json +19 -2
|
@@ -0,0 +1,1945 @@
|
|
|
1
|
+
// src/source-analysis/ast.ts
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
function scriptKindFor(fileName) {
|
|
4
|
+
if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
5
|
+
if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
|
|
6
|
+
if (fileName.endsWith(".mts") || fileName.endsWith(".cts") || fileName.endsWith(".ts")) return ts.ScriptKind.TS;
|
|
7
|
+
return ts.ScriptKind.JS;
|
|
8
|
+
}
|
|
9
|
+
function parseSource(fileName, source) {
|
|
10
|
+
const sourceFile = ts.createSourceFile(
|
|
11
|
+
fileName,
|
|
12
|
+
source,
|
|
13
|
+
ts.ScriptTarget.Latest,
|
|
14
|
+
/*setParentNodes*/
|
|
15
|
+
true,
|
|
16
|
+
scriptKindFor(fileName)
|
|
17
|
+
);
|
|
18
|
+
const diags = sourceFile.parseDiagnostics ?? [];
|
|
19
|
+
return { sourceFile, parseOk: diags.length === 0 };
|
|
20
|
+
}
|
|
21
|
+
function lineOf(sourceFile, pos) {
|
|
22
|
+
return sourceFile.getLineAndCharacterOfPosition(pos).line + 1;
|
|
23
|
+
}
|
|
24
|
+
function commentInNodeMatches(node, sourceFile, source, pattern) {
|
|
25
|
+
const start = node.getStart(sourceFile);
|
|
26
|
+
const text = source.slice(start, node.getEnd());
|
|
27
|
+
const scanner = ts.createScanner(
|
|
28
|
+
ts.ScriptTarget.Latest,
|
|
29
|
+
/*skipTrivia*/
|
|
30
|
+
false,
|
|
31
|
+
ts.LanguageVariant.Standard,
|
|
32
|
+
text
|
|
33
|
+
);
|
|
34
|
+
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
35
|
+
if (token === ts.SyntaxKind.SingleLineCommentTrivia || token === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
36
|
+
if (pattern.test(scanner.getTokenText())) return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/invariant.ts
|
|
43
|
+
var InvariantError = class extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "InvariantError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/source-analysis/mutation-recipes.ts
|
|
51
|
+
import { createHash } from "crypto";
|
|
52
|
+
function sha256(text) {
|
|
53
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
54
|
+
}
|
|
55
|
+
function offsetOf(source, line, column) {
|
|
56
|
+
let lineStart = 0;
|
|
57
|
+
let curLine = 1;
|
|
58
|
+
for (let i = 0; i < source.length && curLine < line; i++) {
|
|
59
|
+
if (source[i] === "\n") {
|
|
60
|
+
lineStart = i + 1;
|
|
61
|
+
curLine++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const off = lineStart + (column - 1);
|
|
65
|
+
return off < 0 ? 0 : off > source.length ? source.length : off;
|
|
66
|
+
}
|
|
67
|
+
function lineEndOffset(source, line) {
|
|
68
|
+
let lineStart = 0;
|
|
69
|
+
let curLine = 1;
|
|
70
|
+
for (let i = 0; i < source.length && curLine < line; i++) {
|
|
71
|
+
if (source[i] === "\n") {
|
|
72
|
+
lineStart = i + 1;
|
|
73
|
+
curLine++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (curLine < line) return -1;
|
|
77
|
+
const nl = source.indexOf("\n", lineStart);
|
|
78
|
+
return nl === -1 ? source.length : nl;
|
|
79
|
+
}
|
|
80
|
+
function strictOffsetOf(source, line, column) {
|
|
81
|
+
if (!Number.isSafeInteger(line) || !Number.isSafeInteger(column) || line < 1 || column < 1) return null;
|
|
82
|
+
const end = lineEndOffset(source, line);
|
|
83
|
+
if (end < 0) return null;
|
|
84
|
+
const start = offsetOf(source, line, 1);
|
|
85
|
+
const off = start + (column - 1);
|
|
86
|
+
return off > end ? null : off;
|
|
87
|
+
}
|
|
88
|
+
function recipeId(file, startOffset, endOffset, original, replacement) {
|
|
89
|
+
return sha256([file, startOffset, endOffset, original, replacement].join("\0"));
|
|
90
|
+
}
|
|
91
|
+
function recipeFromSpan(file, source, span, replacement, category) {
|
|
92
|
+
const r = tryRecipeFromSpan(file, source, span, replacement, category);
|
|
93
|
+
if (!r) throw new InvariantError(`span out of range for ${file}: ${JSON.stringify(span)}`);
|
|
94
|
+
return r;
|
|
95
|
+
}
|
|
96
|
+
function tryRecipeFromSpan(file, source, span, replacement, category) {
|
|
97
|
+
const startOffset = strictOffsetOf(source, span.startLine, span.startColumn);
|
|
98
|
+
const endOffset = strictOffsetOf(source, span.endLine, span.endColumn);
|
|
99
|
+
if (startOffset === null || endOffset === null || endOffset < startOffset) return null;
|
|
100
|
+
const original = source.slice(startOffset, endOffset);
|
|
101
|
+
return {
|
|
102
|
+
file,
|
|
103
|
+
startOffset,
|
|
104
|
+
endOffset,
|
|
105
|
+
original,
|
|
106
|
+
replacement,
|
|
107
|
+
sourceDigest: sha256(source),
|
|
108
|
+
id: recipeId(file, startOffset, endOffset, original, replacement),
|
|
109
|
+
...category !== void 0 ? { category } : {}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function recipeFromOffsets(file, source, startOffset, endOffset, replacement, category) {
|
|
113
|
+
const original = source.slice(startOffset, endOffset);
|
|
114
|
+
return {
|
|
115
|
+
file,
|
|
116
|
+
startOffset,
|
|
117
|
+
endOffset,
|
|
118
|
+
original,
|
|
119
|
+
replacement,
|
|
120
|
+
sourceDigest: sha256(source),
|
|
121
|
+
id: recipeId(file, startOffset, endOffset, original, replacement),
|
|
122
|
+
...category !== void 0 ? { category } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function applyRecipe(source, recipe) {
|
|
126
|
+
if (recipe.startOffset < 0 || recipe.endOffset < recipe.startOffset || recipe.endOffset > source.length) {
|
|
127
|
+
return { ok: false, reason: "bad-span" };
|
|
128
|
+
}
|
|
129
|
+
if (sha256(source) !== recipe.sourceDigest) return { ok: false, reason: "source-drift" };
|
|
130
|
+
if (source.slice(recipe.startOffset, recipe.endOffset) !== recipe.original) return { ok: false, reason: "slice-mismatch" };
|
|
131
|
+
return { ok: true, result: source.slice(0, recipe.startOffset) + recipe.replacement + source.slice(recipe.endOffset) };
|
|
132
|
+
}
|
|
133
|
+
function escapeMutatePath(path) {
|
|
134
|
+
return path.replace(/[[\]{}*?!+@()|]/g, (c) => `\\${c}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/source-analysis/error-handlers.ts
|
|
138
|
+
var TODO_RE = /\b(?:TODO|FIXME)\b/;
|
|
139
|
+
function isAbortCall(expr) {
|
|
140
|
+
if (!ts.isCallExpression(expr)) return false;
|
|
141
|
+
const callee = expr.expression;
|
|
142
|
+
if (!ts.isPropertyAccessExpression(callee)) return false;
|
|
143
|
+
const obj = callee.expression;
|
|
144
|
+
if (!ts.isIdentifier(obj)) return false;
|
|
145
|
+
const method = callee.name.text;
|
|
146
|
+
const holder = obj.text;
|
|
147
|
+
return holder === "process" && (method === "exit" || method === "abort") || holder === "Deno" && method === "exit" || holder === "Bun" && method === "exit";
|
|
148
|
+
}
|
|
149
|
+
function hasHandlerLevelAbort(block) {
|
|
150
|
+
for (const stmt of block.statements) {
|
|
151
|
+
if (ts.isExpressionStatement(stmt) && isAbortCall(stmt.expression)) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
function scanFileHandlers(fileName, source, changedLines) {
|
|
156
|
+
const parsed = parseSource(fileName, source);
|
|
157
|
+
if (!parsed.parseOk) return { antiPatterns: [], changedHandlers: [], parseOk: false };
|
|
158
|
+
const sf = parsed.sourceFile;
|
|
159
|
+
const antiPatterns = [];
|
|
160
|
+
const changedHandlers = [];
|
|
161
|
+
const intersectsChange = (startLine, endLine) => {
|
|
162
|
+
for (let l = startLine; l <= endLine; l++) if (changedLines.has(l)) return true;
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
const visit = (node) => {
|
|
166
|
+
if (ts.isCatchClause(node)) {
|
|
167
|
+
const startLine = lineOf(sf, node.getStart(sf));
|
|
168
|
+
const endLine = lineOf(sf, node.getEnd());
|
|
169
|
+
if (intersectsChange(startLine, endLine)) {
|
|
170
|
+
changedHandlers.push({ file: fileName, startLine, endLine });
|
|
171
|
+
const push = (kind) => antiPatterns.push({ file: fileName, startLine, endLine, kind });
|
|
172
|
+
if (node.block.statements.length === 0) push("empty-catch");
|
|
173
|
+
if (hasHandlerLevelAbort(node.block)) push("catch-all-abort");
|
|
174
|
+
if (commentInNodeMatches(node, sf, source, TODO_RE)) push("todo-in-handler");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
ts.forEachChild(node, visit);
|
|
178
|
+
};
|
|
179
|
+
visit(sf);
|
|
180
|
+
return { antiPatterns, changedHandlers, parseOk: true };
|
|
181
|
+
}
|
|
182
|
+
function forcedHandlerRecipes(fileName, source, changedLines) {
|
|
183
|
+
const parsed = parseSource(fileName, source);
|
|
184
|
+
if (!parsed.parseOk) return [];
|
|
185
|
+
const sf = parsed.sourceFile;
|
|
186
|
+
const recipes = [];
|
|
187
|
+
const intersectsChange = (startLine, endLine) => {
|
|
188
|
+
for (let l = startLine; l <= endLine; l++) if (changedLines.has(l)) return true;
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
const visit = (node) => {
|
|
192
|
+
if (ts.isCatchClause(node)) {
|
|
193
|
+
const startLine = lineOf(sf, node.getStart(sf));
|
|
194
|
+
const endLine = lineOf(sf, node.getEnd());
|
|
195
|
+
if (intersectsChange(startLine, endLine)) {
|
|
196
|
+
const target = node.block.statements.find((st) => {
|
|
197
|
+
if (ts.isEmptyStatement(st)) return false;
|
|
198
|
+
if (source.slice(st.getStart(sf), st.getEnd()).trim() === ";") return false;
|
|
199
|
+
const s0 = lineOf(sf, st.getStart(sf));
|
|
200
|
+
const e0 = lineOf(sf, st.getEnd());
|
|
201
|
+
for (let l = s0; l <= e0; l++) if (!changedLines.has(l)) return false;
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
if (target) {
|
|
205
|
+
const s = target.getStart(sf);
|
|
206
|
+
const e = target.getEnd();
|
|
207
|
+
recipes.push(recipeFromOffsets(fileName, source, s, e, ";", "error-handler"));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
ts.forEachChild(node, visit);
|
|
212
|
+
};
|
|
213
|
+
visit(sf);
|
|
214
|
+
recipes.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.startOffset - b.startOffset);
|
|
215
|
+
return recipes;
|
|
216
|
+
}
|
|
217
|
+
function scanErrorHandlers(inputs) {
|
|
218
|
+
const antiPatterns = [];
|
|
219
|
+
const changedHandlers = [];
|
|
220
|
+
let quality = "complete";
|
|
221
|
+
for (const { file, source, changedLines } of inputs) {
|
|
222
|
+
const r = scanFileHandlers(file, source, changedLines);
|
|
223
|
+
if (!r.parseOk) {
|
|
224
|
+
quality = "partial";
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
antiPatterns.push(...r.antiPatterns);
|
|
228
|
+
changedHandlers.push(...r.changedHandlers);
|
|
229
|
+
}
|
|
230
|
+
const key = (h) => `${h.file}:${h.startLine}:${h.endLine}`;
|
|
231
|
+
antiPatterns.sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0);
|
|
232
|
+
changedHandlers.sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
233
|
+
return { antiPatterns, changedHandlers, quality };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/workdir-path.ts
|
|
237
|
+
import { isAbsolute, relative, sep } from "path";
|
|
238
|
+
function toPosixSeparators(file) {
|
|
239
|
+
return sep === "\\" ? file.split("\\").join("/") : file;
|
|
240
|
+
}
|
|
241
|
+
function toWorkdirRelPosix(file, workDir) {
|
|
242
|
+
if (!file) return file;
|
|
243
|
+
let f = file;
|
|
244
|
+
if (workDir && isAbsolute(f)) {
|
|
245
|
+
const rel = relative(workDir, f);
|
|
246
|
+
if (rel.length > 0 && !rel.startsWith("..")) f = rel;
|
|
247
|
+
}
|
|
248
|
+
return toPosixSeparators(f);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/source-analysis/test-analysis.ts
|
|
252
|
+
var EXPECT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
253
|
+
var ASSERT_EQ_METHODS = /* @__PURE__ */ new Set(["equal", "strictEqual", "deepEqual", "deepStrictEqual", "deepEqual"]);
|
|
254
|
+
var ASSERT_TRUTHY_METHODS = /* @__PURE__ */ new Set(["ok", "isTrue"]);
|
|
255
|
+
function sameText(a, b, sf) {
|
|
256
|
+
if (a.getText(sf).trim() !== b.getText(sf).trim()) return false;
|
|
257
|
+
return isPurelyReferential(a) && isPurelyReferential(b);
|
|
258
|
+
}
|
|
259
|
+
function isPurelyReferential(n) {
|
|
260
|
+
let ok = true;
|
|
261
|
+
const visit = (node) => {
|
|
262
|
+
if (!ok) return;
|
|
263
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node) || ts.isAwaitExpression(node) || ts.isTaggedTemplateExpression(node) || ts.isPropertyAccessExpression(node) || // may hit a getter / Proxy trap
|
|
264
|
+
ts.isElementAccessExpression(node) || ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node) || // ++x / x-- mutate between evaluations
|
|
265
|
+
ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
266
|
+
ok = false;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
ts.forEachChild(node, visit);
|
|
270
|
+
};
|
|
271
|
+
visit(n);
|
|
272
|
+
return ok;
|
|
273
|
+
}
|
|
274
|
+
function isTrueLiteral(n) {
|
|
275
|
+
return n.kind === ts.SyntaxKind.TrueKeyword;
|
|
276
|
+
}
|
|
277
|
+
function classifyCall(call, sf) {
|
|
278
|
+
const callee = call.expression;
|
|
279
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
280
|
+
const obj = callee.expression;
|
|
281
|
+
const method = callee.name.text;
|
|
282
|
+
if (ts.isCallExpression(obj) && ts.isIdentifier(obj.expression) && obj.expression.text === "expect") {
|
|
283
|
+
if (!EXPECT_MATCHERS.has(method)) return "unknown";
|
|
284
|
+
const lhs = obj.arguments[0];
|
|
285
|
+
const rhs = call.arguments[0];
|
|
286
|
+
if (!lhs || !rhs) return "unknown";
|
|
287
|
+
return sameText(lhs, rhs, sf) ? "tautology" : "non-tautology";
|
|
288
|
+
}
|
|
289
|
+
if (ts.isIdentifier(obj) && obj.text === "assert") {
|
|
290
|
+
if (ASSERT_EQ_METHODS.has(method)) {
|
|
291
|
+
const a = call.arguments[0];
|
|
292
|
+
const b = call.arguments[1];
|
|
293
|
+
if (!a || !b) return "unknown";
|
|
294
|
+
return sameText(a, b, sf) ? "tautology" : "non-tautology";
|
|
295
|
+
}
|
|
296
|
+
if (ASSERT_TRUTHY_METHODS.has(method)) {
|
|
297
|
+
const a = call.arguments[0];
|
|
298
|
+
return a && isTrueLiteral(a) ? "tautology" : "non-tautology";
|
|
299
|
+
}
|
|
300
|
+
return "unknown";
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (ts.isIdentifier(callee) && callee.text === "assert") {
|
|
305
|
+
const a = call.arguments[0];
|
|
306
|
+
return a && isTrueLiteral(a) ? "tautology" : "non-tautology";
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
var TEST_FNS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
311
|
+
var DESCRIBE_FNS = /* @__PURE__ */ new Set(["describe", "suite"]);
|
|
312
|
+
var TEST_MODIFIERS = /* @__PURE__ */ new Set(["only", "skip", "todo", "failing", "concurrent", "each", "fails"]);
|
|
313
|
+
function isParameterized(expr) {
|
|
314
|
+
let node = expr;
|
|
315
|
+
while (ts.isCallExpression(node)) node = node.expression;
|
|
316
|
+
while (ts.isPropertyAccessExpression(node)) {
|
|
317
|
+
if (node.name.text === "each") return true;
|
|
318
|
+
node = node.expression;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function testCallName(expr) {
|
|
323
|
+
let node = expr;
|
|
324
|
+
while (ts.isCallExpression(node)) node = node.expression;
|
|
325
|
+
while (ts.isPropertyAccessExpression(node) && TEST_MODIFIERS.has(node.name.text)) node = node.expression;
|
|
326
|
+
if (ts.isIdentifier(node)) return node.text;
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
function stringArg(call) {
|
|
330
|
+
const a = call.arguments[0];
|
|
331
|
+
if (a && ts.isStringLiteralLike(a)) return a.text;
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function scanFile(fileName, source, out, workDir) {
|
|
335
|
+
const parsed = parseSource(fileName, source);
|
|
336
|
+
if (!parsed.parseOk) return false;
|
|
337
|
+
const sf = parsed.sourceFile;
|
|
338
|
+
const relFile = toWorkdirRelPosix(fileName, workDir);
|
|
339
|
+
let parameterizedSkipped = false;
|
|
340
|
+
const walkTestBody = (body) => {
|
|
341
|
+
let taut = 0;
|
|
342
|
+
let real = 0;
|
|
343
|
+
let unknown = 0;
|
|
344
|
+
const visit2 = (n) => {
|
|
345
|
+
if (ts.isCallExpression(n)) {
|
|
346
|
+
const inner = testCallName(n.expression);
|
|
347
|
+
if (inner !== null && (TEST_FNS.has(inner) || DESCRIBE_FNS.has(inner))) return;
|
|
348
|
+
}
|
|
349
|
+
if (ts.isCallExpression(n)) {
|
|
350
|
+
const cls = classifyCall(n, sf);
|
|
351
|
+
if (cls === "tautology") taut++;
|
|
352
|
+
else if (cls === "non-tautology") real++;
|
|
353
|
+
else if (cls === "unknown") unknown++;
|
|
354
|
+
}
|
|
355
|
+
ts.forEachChild(n, visit2);
|
|
356
|
+
};
|
|
357
|
+
ts.forEachChild(body, visit2);
|
|
358
|
+
return { taut, real, unknown };
|
|
359
|
+
};
|
|
360
|
+
const visit = (node, describeStack) => {
|
|
361
|
+
if (ts.isCallExpression(node) && testCallName(node.expression) !== null) {
|
|
362
|
+
const fn = testCallName(node.expression);
|
|
363
|
+
const title = stringArg(node);
|
|
364
|
+
const cb = node.arguments.find((a) => ts.isFunctionExpression(a) || ts.isArrowFunction(a));
|
|
365
|
+
if (DESCRIBE_FNS.has(fn) && title !== null && cb?.body) {
|
|
366
|
+
const nested = [...describeStack, title];
|
|
367
|
+
ts.forEachChild(cb.body, (c) => visit(c, nested));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (TEST_FNS.has(fn) && isParameterized(node.expression)) {
|
|
371
|
+
parameterizedSkipped = true;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (TEST_FNS.has(fn) && title !== null && cb?.body) {
|
|
375
|
+
const fullName = [...describeStack, title].join(" ");
|
|
376
|
+
const { taut, real, unknown } = walkTestBody(cb.body);
|
|
377
|
+
if (taut > 0 && real === 0 && unknown === 0) out.push(`${relFile}::${fullName}`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
ts.forEachChild(node, (c) => visit(c, describeStack));
|
|
382
|
+
};
|
|
383
|
+
visit(sf, []);
|
|
384
|
+
return !parameterizedSkipped;
|
|
385
|
+
}
|
|
386
|
+
function scanTautologies(inputs, workDir) {
|
|
387
|
+
const tautologicalTests = [];
|
|
388
|
+
let quality = inputs.length === 0 ? "partial" : "complete";
|
|
389
|
+
for (const { file, source } of inputs) {
|
|
390
|
+
if (!scanFile(file, source, tautologicalTests, workDir)) quality = "partial";
|
|
391
|
+
}
|
|
392
|
+
tautologicalTests.sort();
|
|
393
|
+
return { tautologicalTests, quality };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/source-analysis/reachability.ts
|
|
397
|
+
import { dirname, isAbsolute as isAbsolute2, resolve as resolvePath } from "path";
|
|
398
|
+
var FUNCTION_LIKE = /* @__PURE__ */ new Set([
|
|
399
|
+
ts.SyntaxKind.FunctionDeclaration,
|
|
400
|
+
ts.SyntaxKind.FunctionExpression,
|
|
401
|
+
ts.SyntaxKind.ArrowFunction,
|
|
402
|
+
ts.SyntaxKind.MethodDeclaration,
|
|
403
|
+
ts.SyntaxKind.Constructor,
|
|
404
|
+
ts.SyntaxKind.GetAccessor,
|
|
405
|
+
ts.SyntaxKind.SetAccessor
|
|
406
|
+
]);
|
|
407
|
+
var SERVICE_MODULES = [
|
|
408
|
+
"pg",
|
|
409
|
+
"postgres",
|
|
410
|
+
"mysql",
|
|
411
|
+
"mysql2",
|
|
412
|
+
"sqlite3",
|
|
413
|
+
"better-sqlite3",
|
|
414
|
+
"mongodb",
|
|
415
|
+
"mongoose",
|
|
416
|
+
"ioredis",
|
|
417
|
+
"redis",
|
|
418
|
+
"@clickhouse/client",
|
|
419
|
+
"kafkajs",
|
|
420
|
+
"amqplib",
|
|
421
|
+
"@prisma/client",
|
|
422
|
+
"aws-sdk",
|
|
423
|
+
"@aws-sdk/",
|
|
424
|
+
"nodemailer",
|
|
425
|
+
"puppeteer",
|
|
426
|
+
"playwright",
|
|
427
|
+
"@playwright/test"
|
|
428
|
+
];
|
|
429
|
+
var BUILTIN_TYPES = /* @__PURE__ */ new Set([
|
|
430
|
+
"Array",
|
|
431
|
+
"Promise",
|
|
432
|
+
"Record",
|
|
433
|
+
"Partial",
|
|
434
|
+
"Required",
|
|
435
|
+
"Readonly",
|
|
436
|
+
"Pick",
|
|
437
|
+
"Omit",
|
|
438
|
+
"Exclude",
|
|
439
|
+
"Extract",
|
|
440
|
+
"ReturnType",
|
|
441
|
+
"Parameters",
|
|
442
|
+
"Map",
|
|
443
|
+
"Set",
|
|
444
|
+
"WeakMap",
|
|
445
|
+
"WeakSet",
|
|
446
|
+
"Date",
|
|
447
|
+
"RegExp",
|
|
448
|
+
"Error",
|
|
449
|
+
"Function",
|
|
450
|
+
"Object",
|
|
451
|
+
"String",
|
|
452
|
+
"Number",
|
|
453
|
+
"Boolean",
|
|
454
|
+
"Symbol",
|
|
455
|
+
"BigInt",
|
|
456
|
+
"Iterable",
|
|
457
|
+
"AsyncIterable",
|
|
458
|
+
"Generator",
|
|
459
|
+
"unknown",
|
|
460
|
+
"any",
|
|
461
|
+
"void",
|
|
462
|
+
"never",
|
|
463
|
+
"null",
|
|
464
|
+
"undefined",
|
|
465
|
+
"string",
|
|
466
|
+
"number",
|
|
467
|
+
"boolean",
|
|
468
|
+
"object",
|
|
469
|
+
"symbol",
|
|
470
|
+
"bigint",
|
|
471
|
+
"this"
|
|
472
|
+
]);
|
|
473
|
+
function enclosingNodes(sf, start, end) {
|
|
474
|
+
const hits = [];
|
|
475
|
+
const visit = (n) => {
|
|
476
|
+
const ns = n.getStart(sf);
|
|
477
|
+
const ne = n.getEnd();
|
|
478
|
+
if (ne < start || ns > end) return;
|
|
479
|
+
if (ns <= start && ne >= end) hits.push(n);
|
|
480
|
+
ts.forEachChild(n, visit);
|
|
481
|
+
};
|
|
482
|
+
visit(sf);
|
|
483
|
+
return hits.filter((n) => !ts.isSourceFile(n)).sort((a, b) => a.getEnd() - a.getStart(sf) - (b.getEnd() - b.getStart(sf)));
|
|
484
|
+
}
|
|
485
|
+
function modifiersOf(node) {
|
|
486
|
+
return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : [];
|
|
487
|
+
}
|
|
488
|
+
function hasModifier(node, kind) {
|
|
489
|
+
return modifiersOf(node).some((m) => m.kind === kind);
|
|
490
|
+
}
|
|
491
|
+
function moduleExportSurface(sourceFile) {
|
|
492
|
+
const out = /* @__PURE__ */ new Set();
|
|
493
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
494
|
+
let hasDefault = false;
|
|
495
|
+
const locals = /* @__PURE__ */ new Map();
|
|
496
|
+
for (const stmt of sourceFile.statements) {
|
|
497
|
+
if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name) {
|
|
498
|
+
locals.set(stmt.name.text, stmt);
|
|
499
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
500
|
+
for (const d of stmt.declarationList.declarations) {
|
|
501
|
+
if (ts.isIdentifier(d.name)) locals.set(d.name.text, d);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const stmt of sourceFile.statements) {
|
|
506
|
+
if (ts.isExportAssignment(stmt)) {
|
|
507
|
+
if (!stmt.isExportEquals) {
|
|
508
|
+
hasDefault = true;
|
|
509
|
+
if (ts.isIdentifier(stmt.expression)) {
|
|
510
|
+
const local = locals.get(stmt.expression.text);
|
|
511
|
+
if (local) declarations.set("default", local);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
517
|
+
for (const el of stmt.exportClause.elements) {
|
|
518
|
+
const localName = (el.propertyName ?? el.name).text;
|
|
519
|
+
const bound = stmt.moduleSpecifier ? void 0 : locals.get(localName);
|
|
520
|
+
if (el.name.text === "default") {
|
|
521
|
+
hasDefault = true;
|
|
522
|
+
if (bound) declarations.set("default", bound);
|
|
523
|
+
} else {
|
|
524
|
+
out.add(el.name.text);
|
|
525
|
+
if (bound) declarations.set(el.name.text, bound);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const mods = modifiersOf(stmt);
|
|
531
|
+
if (!mods.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
532
|
+
if (mods.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
533
|
+
hasDefault = true;
|
|
534
|
+
declarations.set("default", stmt);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name) {
|
|
538
|
+
out.add(stmt.name.text);
|
|
539
|
+
declarations.set(stmt.name.text, stmt);
|
|
540
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
541
|
+
for (const d of stmt.declarationList.declarations) {
|
|
542
|
+
if (ts.isIdentifier(d.name)) {
|
|
543
|
+
out.add(d.name.text);
|
|
544
|
+
declarations.set(d.name.text, d);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { named: [...out].sort(), hasDefault, declarations };
|
|
550
|
+
}
|
|
551
|
+
function functionOf(decl) {
|
|
552
|
+
if (FUNCTION_LIKE.has(decl.kind)) return decl;
|
|
553
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer && FUNCTION_LIKE.has(decl.initializer.kind)) {
|
|
554
|
+
return decl.initializer;
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
function nameOfFunction(node, sf) {
|
|
559
|
+
if (ts.isFunctionDeclaration(node) && node.name) return node.name.text;
|
|
560
|
+
if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node)) {
|
|
561
|
+
const owner = node.parent && ts.isClassDeclaration(node.parent) ? node.parent.name?.text : void 0;
|
|
562
|
+
const member = ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;
|
|
563
|
+
if (!member) return null;
|
|
564
|
+
return owner ? `${owner}.${member}` : member;
|
|
565
|
+
}
|
|
566
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
567
|
+
const owner = node.parent && ts.isClassDeclaration(node.parent) ? node.parent.name?.text : void 0;
|
|
568
|
+
return owner ? `${owner}.constructor` : "constructor";
|
|
569
|
+
}
|
|
570
|
+
const parent = node.parent;
|
|
571
|
+
if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
|
|
572
|
+
if (parent && ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
|
|
573
|
+
void sf;
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function collectTypeNames(node, into) {
|
|
577
|
+
if (!node) return;
|
|
578
|
+
const visit = (n) => {
|
|
579
|
+
if (ts.isTypeReferenceNode(n) && ts.isIdentifier(n.typeName)) {
|
|
580
|
+
const name = n.typeName.text;
|
|
581
|
+
if (!BUILTIN_TYPES.has(name)) into.add(name);
|
|
582
|
+
}
|
|
583
|
+
ts.forEachChild(n, visit);
|
|
584
|
+
};
|
|
585
|
+
visit(node);
|
|
586
|
+
}
|
|
587
|
+
function signatureOf(fn, sf, name) {
|
|
588
|
+
const parameters = fn.parameters.map((p) => ({
|
|
589
|
+
// a destructuring pattern keeps its literal text — it shows which keys the code reads
|
|
590
|
+
name: p.name.getText(sf),
|
|
591
|
+
optional: p.questionToken !== void 0 || p.initializer !== void 0,
|
|
592
|
+
type: p.type ? p.type.getText(sf).slice(0, 200) : null,
|
|
593
|
+
rest: p.dotDotDotToken !== void 0
|
|
594
|
+
}));
|
|
595
|
+
const returnType = fn.type ? fn.type.getText(sf).slice(0, 200) : null;
|
|
596
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
597
|
+
for (const p of fn.parameters) collectTypeNames(p.type, typeNames);
|
|
598
|
+
collectTypeNames(fn.type, typeNames);
|
|
599
|
+
const rendered = parameters.map((p) => `${p.rest ? "..." : ""}${p.name}${p.optional ? "?" : ""}${p.type ? `: ${p.type}` : ""}`).join(", ");
|
|
600
|
+
return {
|
|
601
|
+
name,
|
|
602
|
+
signature: `${name}(${rendered})${returnType ? `: ${returnType}` : ""}`,
|
|
603
|
+
parameters,
|
|
604
|
+
returnType,
|
|
605
|
+
isAsync: hasModifier(fn, ts.SyntaxKind.AsyncKeyword),
|
|
606
|
+
typeNames: [...typeNames]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function buildCallGraph(sf) {
|
|
610
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
611
|
+
const collect = (n) => {
|
|
612
|
+
if (FUNCTION_LIKE.has(n.kind)) {
|
|
613
|
+
const name = nameOfFunction(n, sf);
|
|
614
|
+
if (name && !nodes.has(name)) nodes.set(name, { name, fn: n, calls: /* @__PURE__ */ new Map() });
|
|
615
|
+
}
|
|
616
|
+
ts.forEachChild(n, collect);
|
|
617
|
+
};
|
|
618
|
+
collect(sf);
|
|
619
|
+
for (const node of nodes.values()) {
|
|
620
|
+
const body = node.fn.body;
|
|
621
|
+
if (!body) continue;
|
|
622
|
+
const visit = (n) => {
|
|
623
|
+
if (ts.isCallExpression(n)) {
|
|
624
|
+
let callee = null;
|
|
625
|
+
if (ts.isIdentifier(n.expression)) callee = n.expression.text;
|
|
626
|
+
else if (ts.isPropertyAccessExpression(n.expression) && n.expression.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
627
|
+
const cls = ts.findAncestor(n, ts.isClassDeclaration)?.name?.text;
|
|
628
|
+
callee = cls ? `${cls}.${n.expression.name.text}` : n.expression.name.text;
|
|
629
|
+
}
|
|
630
|
+
if (callee && !node.calls.has(callee)) {
|
|
631
|
+
const line = lineOf(sf, n.getStart(sf));
|
|
632
|
+
const text = sf.text.split("\n")[line - 1]?.trim() ?? "";
|
|
633
|
+
node.calls.set(callee, `${line}: ${text.slice(0, 200)}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (ts.isIdentifier(n) && nodes.has(n.text) && n.text !== node.name) {
|
|
637
|
+
const p = n.parent;
|
|
638
|
+
const isMemberName = p !== void 0 && ts.isPropertyAccessExpression(p) && p.name === n;
|
|
639
|
+
const isObjectKey = p !== void 0 && (ts.isPropertyAssignment(p) || ts.isPropertySignature(p)) && p.name === n;
|
|
640
|
+
const isDeclName = p !== void 0 && p.name === n && !ts.isCallExpression(p);
|
|
641
|
+
if (!isMemberName && !isObjectKey && !isDeclName && !node.calls.has(n.text)) {
|
|
642
|
+
const line = lineOf(sf, n.getStart(sf));
|
|
643
|
+
node.calls.set(n.text, `${line}: ${sf.text.split("\n")[line - 1]?.trim().slice(0, 200) ?? ""}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (n !== body && FUNCTION_LIKE.has(n.kind)) {
|
|
647
|
+
const nested = nameOfFunction(n, sf);
|
|
648
|
+
if (nested) {
|
|
649
|
+
if (!node.calls.has(nested) && nested !== node.name) {
|
|
650
|
+
const line = lineOf(sf, n.getStart(sf));
|
|
651
|
+
node.calls.set(nested, `${line}: ${sf.text.split("\n")[line - 1]?.trim().slice(0, 200) ?? ""}`);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
ts.forEachChild(n, visit);
|
|
657
|
+
};
|
|
658
|
+
ts.forEachChild(body, visit);
|
|
659
|
+
}
|
|
660
|
+
return nodes;
|
|
661
|
+
}
|
|
662
|
+
var UNAVAILABLE = (surface) => ({
|
|
663
|
+
quality: "unavailable",
|
|
664
|
+
reachability: "unknown",
|
|
665
|
+
enclosing: null,
|
|
666
|
+
entryPoints: [],
|
|
667
|
+
exportSurface: surface,
|
|
668
|
+
serviceImports: []
|
|
669
|
+
});
|
|
670
|
+
var EMPTY_SURFACE = { named: [], hasDefault: false, declarations: /* @__PURE__ */ new Map() };
|
|
671
|
+
function importedSpecifiers(sf) {
|
|
672
|
+
const out = [];
|
|
673
|
+
const visit = (n) => {
|
|
674
|
+
if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) out.push(n.moduleSpecifier.text);
|
|
675
|
+
else if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "require" && n.arguments.length === 1 && ts.isStringLiteral(n.arguments[0])) {
|
|
676
|
+
out.push(n.arguments[0].text);
|
|
677
|
+
}
|
|
678
|
+
ts.forEachChild(n, visit);
|
|
679
|
+
};
|
|
680
|
+
visit(sf);
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
function analyzeReachability(input) {
|
|
684
|
+
const maxEntryPoints = input.maxEntryPoints ?? 3;
|
|
685
|
+
const maxDepth = input.maxDepth ?? 4;
|
|
686
|
+
let parsed;
|
|
687
|
+
try {
|
|
688
|
+
parsed = parseSource(input.fileName, input.source);
|
|
689
|
+
} catch {
|
|
690
|
+
return UNAVAILABLE(EMPTY_SURFACE);
|
|
691
|
+
}
|
|
692
|
+
if (!parsed.parseOk) return UNAVAILABLE(EMPTY_SURFACE);
|
|
693
|
+
const sf = parsed.sourceFile;
|
|
694
|
+
let surface;
|
|
695
|
+
try {
|
|
696
|
+
surface = moduleExportSurface(sf);
|
|
697
|
+
} catch {
|
|
698
|
+
return UNAVAILABLE(EMPTY_SURFACE);
|
|
699
|
+
}
|
|
700
|
+
const serviceImports = [...new Set(importedSpecifiers(sf).filter((spec) => SERVICE_MODULES.some((m) => spec === m || spec.startsWith(m))))];
|
|
701
|
+
const containers = enclosingNodes(sf, input.startOffset, input.endOffset);
|
|
702
|
+
const fnNode = containers.find((n) => FUNCTION_LIKE.has(n.kind));
|
|
703
|
+
if (!fnNode) {
|
|
704
|
+
return {
|
|
705
|
+
quality: "complete",
|
|
706
|
+
reachability: "exported-directly",
|
|
707
|
+
enclosing: null,
|
|
708
|
+
entryPoints: [],
|
|
709
|
+
exportSurface: surface,
|
|
710
|
+
serviceImports
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const fnName = nameOfFunction(fnNode, sf);
|
|
714
|
+
const enclosing = {
|
|
715
|
+
name: fnName,
|
|
716
|
+
// getStart(sf, true) includes leading JSDoc — the comment often states the contract the test
|
|
717
|
+
// must exercise, and dropping it loses the most useful sentence in the file
|
|
718
|
+
text: sf.text.slice(fnNode.getStart(sf, true), fnNode.getEnd()),
|
|
719
|
+
startLine: lineOf(sf, fnNode.getStart(sf)),
|
|
720
|
+
endLine: lineOf(sf, fnNode.getEnd()),
|
|
721
|
+
exported: false,
|
|
722
|
+
typeNames: (() => {
|
|
723
|
+
const into = /* @__PURE__ */ new Set();
|
|
724
|
+
const sig = fnNode;
|
|
725
|
+
for (const p of sig.parameters ?? []) collectTypeNames(p.type, into);
|
|
726
|
+
collectTypeNames(sig.type, into);
|
|
727
|
+
return [...into];
|
|
728
|
+
})()
|
|
729
|
+
};
|
|
730
|
+
const exportedNames = /* @__PURE__ */ new Set([...surface.declarations.keys()]);
|
|
731
|
+
for (const stmt of sf.statements) {
|
|
732
|
+
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals && ts.isIdentifier(stmt.expression)) exportedNames.add(stmt.expression.text);
|
|
733
|
+
}
|
|
734
|
+
const isExportedLocally = (name) => exportedNames.has(name) || surface.named.includes(name);
|
|
735
|
+
const directName = fnName && exportedNames.has(fnName) ? fnName : fnName && surface.named.includes(fnName) ? fnName : null;
|
|
736
|
+
if (directName) {
|
|
737
|
+
enclosing.exported = true;
|
|
738
|
+
const fn = functionOf(surface.declarations.get(directName) ?? fnNode) ?? fnNode;
|
|
739
|
+
return {
|
|
740
|
+
quality: "complete",
|
|
741
|
+
reachability: "exported-directly",
|
|
742
|
+
enclosing,
|
|
743
|
+
entryPoints: [{ ...signatureOf(fn, sf, directName), kind: "function", path: [directName], callSites: [] }],
|
|
744
|
+
exportSurface: surface,
|
|
745
|
+
serviceImports
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (!fnName) {
|
|
749
|
+
return { quality: "unavailable", reachability: "unknown", enclosing, entryPoints: [], exportSurface: surface, serviceImports };
|
|
750
|
+
}
|
|
751
|
+
const graph = buildCallGraph(sf);
|
|
752
|
+
const roots = [...surface.declarations.entries()].map(([name, decl]) => ({ name, node: functionOf(decl) ? nameOfFunction(functionOf(decl), sf) ?? name : name })).sort((a, b) => a.name.localeCompare(b.name));
|
|
753
|
+
const routesTo = (target, tail = []) => {
|
|
754
|
+
const out = [];
|
|
755
|
+
for (const root of roots) {
|
|
756
|
+
if (out.length >= maxEntryPoints) break;
|
|
757
|
+
const start = graph.get(root.node) ?? graph.get(root.name);
|
|
758
|
+
if (!start) continue;
|
|
759
|
+
const queue = [{ name: start.name, path: [start.name], sites: [] }];
|
|
760
|
+
const seen = /* @__PURE__ */ new Set([start.name]);
|
|
761
|
+
while (queue.length > 0) {
|
|
762
|
+
const cur = queue.shift();
|
|
763
|
+
if (cur.path.length > maxDepth) continue;
|
|
764
|
+
if (cur.name === target) {
|
|
765
|
+
const decl = surface.declarations.get(root.name);
|
|
766
|
+
const fn = (decl && functionOf(decl)) ?? start.fn;
|
|
767
|
+
out.push({
|
|
768
|
+
...signatureOf(fn, sf, root.name),
|
|
769
|
+
kind: decl && ts.isVariableDeclaration(decl) ? "variable" : root.name === "default" ? "default" : ts.isMethodDeclaration(fn) ? "method" : "function",
|
|
770
|
+
path: [...cur.path, ...tail],
|
|
771
|
+
callSites: cur.sites
|
|
772
|
+
});
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
const node = graph.get(cur.name);
|
|
776
|
+
if (!node) continue;
|
|
777
|
+
for (const [callee, site] of node.calls) {
|
|
778
|
+
if (seen.has(callee)) continue;
|
|
779
|
+
seen.add(callee);
|
|
780
|
+
queue.push({ name: callee, path: [...cur.path, callee], sites: [...cur.sites, site] });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return out;
|
|
785
|
+
};
|
|
786
|
+
const found = routesTo(fnName);
|
|
787
|
+
if (found.length === 0) {
|
|
788
|
+
const ancestors = containers.filter((n) => n !== fnNode && FUNCTION_LIKE.has(n.kind));
|
|
789
|
+
const tail = [fnName];
|
|
790
|
+
for (const anc of ancestors) {
|
|
791
|
+
const ancName = nameOfFunction(anc, sf);
|
|
792
|
+
if (!ancName) continue;
|
|
793
|
+
if (isExportedLocally(ancName)) {
|
|
794
|
+
const decl = surface.declarations.get(ancName);
|
|
795
|
+
const fn = (decl && functionOf(decl)) ?? anc;
|
|
796
|
+
found.push({
|
|
797
|
+
...signatureOf(fn, sf, ancName),
|
|
798
|
+
kind: decl && ts.isVariableDeclaration(decl) ? "variable" : "function",
|
|
799
|
+
path: [ancName, ...tail],
|
|
800
|
+
callSites: []
|
|
801
|
+
});
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
found.push(...routesTo(ancName, tail));
|
|
805
|
+
if (found.length > 0) break;
|
|
806
|
+
tail.unshift(ancName);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (found.length === 0) {
|
|
810
|
+
const cls = ts.findAncestor(fnNode, ts.isClassDeclaration);
|
|
811
|
+
const clsName = cls?.name?.text;
|
|
812
|
+
if (clsName && isExportedLocally(clsName)) {
|
|
813
|
+
found.push({
|
|
814
|
+
...signatureOf(fnNode, sf, fnName),
|
|
815
|
+
kind: "method",
|
|
816
|
+
path: [clsName, fnName],
|
|
817
|
+
callSites: []
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (found.length === 0) {
|
|
822
|
+
const owner = ts.findAncestor(fnNode, (n) => {
|
|
823
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) return isExportedLocally(n.name.text);
|
|
824
|
+
if ((ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && n.name) return isExportedLocally(n.name.text);
|
|
825
|
+
return false;
|
|
826
|
+
});
|
|
827
|
+
const ownerName = owner ? owner.name?.text ?? null : null;
|
|
828
|
+
if (ownerName) {
|
|
829
|
+
found.push({
|
|
830
|
+
...signatureOf(fnNode, sf, fnName),
|
|
831
|
+
kind: "variable",
|
|
832
|
+
path: [ownerName, fnName],
|
|
833
|
+
callSites: []
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
found.sort((a, b) => a.path.length - b.path.length || a.name.localeCompare(b.name));
|
|
838
|
+
const referencedElsewhere = () => {
|
|
839
|
+
const own = fnNode.getStart(sf);
|
|
840
|
+
const ownEnd = fnNode.getEnd();
|
|
841
|
+
const bare = fnName.includes(".") ? fnName.slice(fnName.lastIndexOf(".") + 1) : fnName;
|
|
842
|
+
let hit = false;
|
|
843
|
+
const scan = (n) => {
|
|
844
|
+
if (hit) return;
|
|
845
|
+
if (ts.isIdentifier(n) && (n.text === fnName || n.text === bare)) {
|
|
846
|
+
const at = n.getStart(sf);
|
|
847
|
+
if (at < own || at >= ownEnd) hit = true;
|
|
848
|
+
}
|
|
849
|
+
ts.forEachChild(n, scan);
|
|
850
|
+
};
|
|
851
|
+
scan(sf);
|
|
852
|
+
return hit;
|
|
853
|
+
};
|
|
854
|
+
const reachability = found.length > 0 ? "reachable-via" : referencedElsewhere() ? "unknown" : "unreachable";
|
|
855
|
+
return {
|
|
856
|
+
quality: "complete",
|
|
857
|
+
reachability,
|
|
858
|
+
enclosing,
|
|
859
|
+
entryPoints: found.slice(0, maxEntryPoints),
|
|
860
|
+
exportSurface: surface,
|
|
861
|
+
serviceImports
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var TS_EXTS = [".ts", ".tsx", ".mts", ".cts", ".d.ts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
865
|
+
function collectTypeContext(input) {
|
|
866
|
+
const maxChars = input.maxChars ?? 6e3;
|
|
867
|
+
const wanted = [...new Set(input.names)].filter((n) => !BUILTIN_TYPES.has(n));
|
|
868
|
+
if (wanted.length === 0) return { text: "", resolved: [], unresolved: [] };
|
|
869
|
+
let parsed;
|
|
870
|
+
try {
|
|
871
|
+
parsed = parseSource(input.fileName, input.source);
|
|
872
|
+
} catch {
|
|
873
|
+
return { text: "", resolved: [], unresolved: [...wanted] };
|
|
874
|
+
}
|
|
875
|
+
if (!parsed.parseOk) return { text: "", resolved: [], unresolved: [...wanted] };
|
|
876
|
+
const sf = parsed.sourceFile;
|
|
877
|
+
const localNode = (file, name) => {
|
|
878
|
+
for (const stmt of file.statements) {
|
|
879
|
+
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name?.text === name) {
|
|
880
|
+
return stmt;
|
|
881
|
+
}
|
|
882
|
+
if (ts.isVariableStatement(stmt)) {
|
|
883
|
+
for (const d of stmt.declarationList.declarations) {
|
|
884
|
+
if (ts.isIdentifier(d.name) && d.name.text === name) return stmt;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
};
|
|
890
|
+
const textOf = (file, stmt) => file.text.slice(stmt.getStart(file, true), stmt.getEnd());
|
|
891
|
+
const localDecl = (file, name) => {
|
|
892
|
+
const stmt = localNode(file, name);
|
|
893
|
+
return stmt ? textOf(file, stmt) : null;
|
|
894
|
+
};
|
|
895
|
+
const deferredTo = (stmt) => {
|
|
896
|
+
if (ts.isInterfaceDeclaration(stmt)) {
|
|
897
|
+
if (stmt.members.length > 0) return [];
|
|
898
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const h of stmt.heritageClauses ?? []) {
|
|
900
|
+
for (const e of h.types) if (ts.isIdentifier(e.expression)) out2.add(e.expression.text);
|
|
901
|
+
}
|
|
902
|
+
return [...out2];
|
|
903
|
+
}
|
|
904
|
+
if (ts.isEnumDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isVariableStatement(stmt)) return [];
|
|
905
|
+
if (!ts.isTypeAliasDeclaration(stmt)) return [];
|
|
906
|
+
if (ts.isTypeLiteralNode(stmt.type) && stmt.type.members.length > 0) return [];
|
|
907
|
+
const out = /* @__PURE__ */ new Set();
|
|
908
|
+
const visit = (n) => {
|
|
909
|
+
if (ts.isTypeReferenceNode(n) && ts.isIdentifier(n.typeName) && !BUILTIN_TYPES.has(n.typeName.text)) out.add(n.typeName.text);
|
|
910
|
+
if (ts.isTypeQueryNode(n) && ts.isIdentifier(n.exprName)) out.add(n.exprName.text);
|
|
911
|
+
ts.forEachChild(n, visit);
|
|
912
|
+
};
|
|
913
|
+
visit(stmt.type);
|
|
914
|
+
out.delete(stmt.name.text);
|
|
915
|
+
return [...out];
|
|
916
|
+
};
|
|
917
|
+
const forwardedFrom = (file, name) => {
|
|
918
|
+
for (const stmt of file.statements) {
|
|
919
|
+
if (!ts.isExportDeclaration(stmt) || !stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
920
|
+
if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue;
|
|
921
|
+
for (const el of stmt.exportClause.elements) {
|
|
922
|
+
if (el.name.text !== name) continue;
|
|
923
|
+
return { spec: stmt.moduleSpecifier.text, original: (el.propertyName ?? el.name).text };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
};
|
|
928
|
+
const importedFrom = /* @__PURE__ */ new Map();
|
|
929
|
+
for (const stmt of sf.statements) {
|
|
930
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
931
|
+
const spec = stmt.moduleSpecifier.text;
|
|
932
|
+
const clause = stmt.importClause;
|
|
933
|
+
if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue;
|
|
934
|
+
for (const el of clause.namedBindings.elements) importedFrom.set(el.name.text, spec);
|
|
935
|
+
}
|
|
936
|
+
const chunks = [];
|
|
937
|
+
const resolved = [];
|
|
938
|
+
const unresolved = [];
|
|
939
|
+
let used = 0;
|
|
940
|
+
const cache = /* @__PURE__ */ new Map();
|
|
941
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
942
|
+
const emitWithShape = (file, stmt, label, depth) => {
|
|
943
|
+
const key = `${file.fileName}\0${stmt.getStart(file)}`;
|
|
944
|
+
if (emitted.has(key)) return true;
|
|
945
|
+
const text = textOf(file, stmt);
|
|
946
|
+
if (used + text.length > maxChars) return false;
|
|
947
|
+
emitted.add(key);
|
|
948
|
+
chunks.push(label ? `// from ${label}
|
|
949
|
+
${text}` : text);
|
|
950
|
+
used += text.length + (label ? label.length + 10 : 0);
|
|
951
|
+
const defers = deferredTo(stmt);
|
|
952
|
+
if (defers.length === 0) return true;
|
|
953
|
+
if (depth <= 0) return false;
|
|
954
|
+
let any = false;
|
|
955
|
+
for (const next of defers) {
|
|
956
|
+
const nextStmt = localNode(file, next);
|
|
957
|
+
if (nextStmt && emitWithShape(file, nextStmt, null, depth - 1)) any = true;
|
|
958
|
+
}
|
|
959
|
+
return any;
|
|
960
|
+
};
|
|
961
|
+
for (const name of wanted) {
|
|
962
|
+
const localStmt = localNode(sf, name);
|
|
963
|
+
if (localStmt) {
|
|
964
|
+
if (emitWithShape(sf, localStmt, null, 2)) resolved.push(name);
|
|
965
|
+
else unresolved.push(name);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const spec = importedFrom.get(name);
|
|
969
|
+
if (!spec || !(spec.startsWith("./") || spec.startsWith("../"))) {
|
|
970
|
+
unresolved.push(name);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const resolveSpec = (fromFile, spec2) => {
|
|
974
|
+
const key = `${fromFile}\0${spec2}`;
|
|
975
|
+
const hit = cache.get(key);
|
|
976
|
+
if (hit !== void 0) return hit;
|
|
977
|
+
let out = null;
|
|
978
|
+
const base = isAbsolute2(spec2) ? spec2 : resolvePath(dirname(fromFile), spec2);
|
|
979
|
+
const jsToTs = {
|
|
980
|
+
".js": [".ts", ".tsx", ".d.ts", ".js", ".jsx"],
|
|
981
|
+
".mjs": [".mts", ".d.mts", ".mjs"],
|
|
982
|
+
".cjs": [".cts", ".d.cts", ".cjs"]
|
|
983
|
+
};
|
|
984
|
+
const extMatch = /\.[a-z]+$/i.exec(base);
|
|
985
|
+
const stripped = extMatch ? base.slice(0, -extMatch[0].length) : base;
|
|
986
|
+
const candidates = extMatch ? [...(jsToTs[extMatch[0].toLowerCase()] ?? []).map((e) => `${stripped}${e}`), base] : [...TS_EXTS.map((e) => `${base}${e}`), ...TS_EXTS.map((e) => `${base}/index${e}`)];
|
|
987
|
+
for (const cand of candidates) {
|
|
988
|
+
const text = input.readFile(cand);
|
|
989
|
+
if (text === null) continue;
|
|
990
|
+
try {
|
|
991
|
+
const p = parseSource(cand, text);
|
|
992
|
+
if (p.parseOk) {
|
|
993
|
+
out = p.sourceFile;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
cache.set(key, out);
|
|
1000
|
+
return out;
|
|
1001
|
+
};
|
|
1002
|
+
let file = resolveSpec(input.fileName, spec);
|
|
1003
|
+
let want = name;
|
|
1004
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1005
|
+
for (let hop = 0; hop < 3 && file; hop += 1) {
|
|
1006
|
+
if (localDecl(file, want)) break;
|
|
1007
|
+
const fwd = forwardedFrom(file, want);
|
|
1008
|
+
if (!fwd || !(fwd.spec.startsWith("./") || fwd.spec.startsWith("../"))) break;
|
|
1009
|
+
const key = `${file.fileName}\0${fwd.spec}\0${fwd.original}`;
|
|
1010
|
+
if (seen.has(key)) break;
|
|
1011
|
+
seen.add(key);
|
|
1012
|
+
const next = resolveSpec(file.fileName, fwd.spec);
|
|
1013
|
+
if (!next) break;
|
|
1014
|
+
file = next;
|
|
1015
|
+
want = fwd.original;
|
|
1016
|
+
}
|
|
1017
|
+
const stmt = file ? localNode(file, want) : null;
|
|
1018
|
+
if (stmt && file && emitWithShape(file, stmt, spec, 2)) resolved.push(name);
|
|
1019
|
+
else unresolved.push(name);
|
|
1020
|
+
}
|
|
1021
|
+
return { text: chunks.join("\n\n"), resolved, unresolved };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/findings.ts
|
|
1025
|
+
var MUTATOR_TOKEN_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/u;
|
|
1026
|
+
|
|
1027
|
+
// src/source-analysis/cluster.ts
|
|
1028
|
+
function locate(source, m) {
|
|
1029
|
+
if (m.startColumn === void 0 || m.endColumn === void 0) {
|
|
1030
|
+
return { reason: "no column recorded \u2014 the engine report carried line granularity only" };
|
|
1031
|
+
}
|
|
1032
|
+
const candidates = [];
|
|
1033
|
+
const push = (sc, ec) => {
|
|
1034
|
+
const s = strictOffsetOf(source, m.startLine, sc);
|
|
1035
|
+
const e = strictOffsetOf(source, m.endLine, ec);
|
|
1036
|
+
if (s !== null && e !== null && e >= s) candidates.push([s, e]);
|
|
1037
|
+
};
|
|
1038
|
+
push(m.startColumn, m.endColumn);
|
|
1039
|
+
push(m.startColumn + 1, m.endColumn + 1);
|
|
1040
|
+
if (candidates.length === 0) return { reason: "column does not lie on its declared line" };
|
|
1041
|
+
if (m.originalText === void 0) {
|
|
1042
|
+
const [s, e] = candidates[0];
|
|
1043
|
+
return { start: s, end: e };
|
|
1044
|
+
}
|
|
1045
|
+
for (const [s, e] of candidates) {
|
|
1046
|
+
if (source.slice(s, e) === m.originalText) return { start: s, end: e };
|
|
1047
|
+
}
|
|
1048
|
+
return { reason: "the recorded original text is not at the reported coordinates (source drift?)" };
|
|
1049
|
+
}
|
|
1050
|
+
function lineKey(m) {
|
|
1051
|
+
return `${m.file} ${m.startLine} ${m.endLine}`;
|
|
1052
|
+
}
|
|
1053
|
+
function statementKey(sf, start, end) {
|
|
1054
|
+
const stmt = enclosingNodes(sf, start, end).find((n) => ts.isStatement(n));
|
|
1055
|
+
if (!stmt) return null;
|
|
1056
|
+
return `${stmt.getStart(sf)}-${stmt.getEnd()}`;
|
|
1057
|
+
}
|
|
1058
|
+
var MAX_CONTAINMENT_CHARS = 2e3;
|
|
1059
|
+
var clusterOf = (by, members) => {
|
|
1060
|
+
const first = members[0];
|
|
1061
|
+
return {
|
|
1062
|
+
id: first.mutantId,
|
|
1063
|
+
file: first.file,
|
|
1064
|
+
startLine: Math.min(...members.map((m) => m.startLine)),
|
|
1065
|
+
endLine: Math.max(...members.map((m) => m.endLine)),
|
|
1066
|
+
members,
|
|
1067
|
+
// a token that is not a bare identifier is dropped from the LIST, never from the COUNT —
|
|
1068
|
+
// the egress validator rejects anything else, and the count is the load-bearing number. The
|
|
1069
|
+
// grammar is core's, because core's validator is what does that rejecting (audit F56).
|
|
1070
|
+
mutators: members.map((m) => m.mutator).filter((t) => MUTATOR_TOKEN_RE.test(t)),
|
|
1071
|
+
by
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
function clusterMutants(mutants, sources, strategy) {
|
|
1075
|
+
const degraded = [];
|
|
1076
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1077
|
+
const put = (key, by, m) => {
|
|
1078
|
+
const b = buckets.get(key);
|
|
1079
|
+
if (b) {
|
|
1080
|
+
b.members.push(m);
|
|
1081
|
+
if (b.by !== by) b.by = "line";
|
|
1082
|
+
} else buckets.set(key, { members: [m], by });
|
|
1083
|
+
};
|
|
1084
|
+
if (strategy === "line") {
|
|
1085
|
+
for (const m of mutants) put(lineKey(m), "line", m);
|
|
1086
|
+
return { clusters: [...buckets].map(([, b]) => clusterOf(b.by, b.members)), requested: strategy, degraded };
|
|
1087
|
+
}
|
|
1088
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1089
|
+
const parseFor = (file) => {
|
|
1090
|
+
if (parsed.has(file)) return parsed.get(file);
|
|
1091
|
+
const source = sources.get(file);
|
|
1092
|
+
let result = null;
|
|
1093
|
+
if (source !== void 0) {
|
|
1094
|
+
const { sourceFile, parseOk } = parseSource(file, source);
|
|
1095
|
+
if (parseOk) result = { sf: sourceFile };
|
|
1096
|
+
}
|
|
1097
|
+
parsed.set(file, result);
|
|
1098
|
+
return result;
|
|
1099
|
+
};
|
|
1100
|
+
const located = [];
|
|
1101
|
+
for (const m of mutants) {
|
|
1102
|
+
const source = sources.get(m.file);
|
|
1103
|
+
if (source === void 0) {
|
|
1104
|
+
degraded.push({ mutantId: m.mutantId, reason: "source not available for this file" });
|
|
1105
|
+
located.push({ mutant: m, key: lineKey(m), by: "line" });
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const p = parseFor(m.file);
|
|
1109
|
+
if (!p) {
|
|
1110
|
+
degraded.push({ mutantId: m.mutantId, reason: "file did not parse cleanly" });
|
|
1111
|
+
located.push({ mutant: m, key: lineKey(m), by: "line" });
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const span = locate(source, m);
|
|
1115
|
+
if ("reason" in span) {
|
|
1116
|
+
degraded.push({ mutantId: m.mutantId, reason: span.reason });
|
|
1117
|
+
located.push({ mutant: m, key: lineKey(m), by: "line" });
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const key = statementKey(p.sf, span.start, span.end);
|
|
1121
|
+
if (key === null) {
|
|
1122
|
+
degraded.push({ mutantId: m.mutantId, reason: "no enclosing statement (mutant outside any statement)" });
|
|
1123
|
+
located.push({ mutant: m, key: lineKey(m), by: "line" });
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const [ks, ke] = key.split("-");
|
|
1127
|
+
located.push({
|
|
1128
|
+
mutant: m,
|
|
1129
|
+
key: `${m.file} stmt ${key}`,
|
|
1130
|
+
by: "structural",
|
|
1131
|
+
stmt: { start: Number(ks), end: Number(ke) }
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const spanOf = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const l of located) if (l.stmt && !spanOf.has(l.key)) spanOf.set(l.key, l.stmt);
|
|
1136
|
+
const foldedInto = /* @__PURE__ */ new Map();
|
|
1137
|
+
const bySize = [...spanOf.entries()].sort(
|
|
1138
|
+
(a, b) => b[1].end - b[1].start - (a[1].end - a[1].start) || (a[0] < b[0] ? -1 : 1)
|
|
1139
|
+
);
|
|
1140
|
+
for (let i = 0; i < bySize.length; i++) {
|
|
1141
|
+
const [outerKey, outer] = bySize[i];
|
|
1142
|
+
if (foldedInto.has(outerKey)) continue;
|
|
1143
|
+
if (outer.end - outer.start > MAX_CONTAINMENT_CHARS) continue;
|
|
1144
|
+
for (let j = i + 1; j < bySize.length; j++) {
|
|
1145
|
+
const [innerKey, inner] = bySize[j];
|
|
1146
|
+
if (foldedInto.has(innerKey)) continue;
|
|
1147
|
+
if (outer.start <= inner.start && outer.end >= inner.end) foldedInto.set(innerKey, outerKey);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const folded = (key) => foldedInto.get(key) ?? key;
|
|
1151
|
+
for (const l of located) put(folded(l.key), l.by, l.mutant);
|
|
1152
|
+
return { clusters: [...buckets].map(([, b]) => clusterOf(b.by, b.members)), requested: strategy, degraded };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/source-analysis/scope.ts
|
|
1156
|
+
var FUNCTION_SCOPE = /* @__PURE__ */ new Set([
|
|
1157
|
+
ts.SyntaxKind.SourceFile,
|
|
1158
|
+
ts.SyntaxKind.FunctionDeclaration,
|
|
1159
|
+
ts.SyntaxKind.FunctionExpression,
|
|
1160
|
+
ts.SyntaxKind.ArrowFunction,
|
|
1161
|
+
ts.SyntaxKind.MethodDeclaration,
|
|
1162
|
+
ts.SyntaxKind.Constructor,
|
|
1163
|
+
ts.SyntaxKind.GetAccessor,
|
|
1164
|
+
ts.SyntaxKind.SetAccessor,
|
|
1165
|
+
ts.SyntaxKind.ModuleDeclaration,
|
|
1166
|
+
ts.SyntaxKind.ClassStaticBlockDeclaration
|
|
1167
|
+
]);
|
|
1168
|
+
var BLOCK_SCOPE = /* @__PURE__ */ new Set([
|
|
1169
|
+
ts.SyntaxKind.Block,
|
|
1170
|
+
ts.SyntaxKind.ForStatement,
|
|
1171
|
+
ts.SyntaxKind.ForInStatement,
|
|
1172
|
+
ts.SyntaxKind.ForOfStatement,
|
|
1173
|
+
ts.SyntaxKind.CatchClause,
|
|
1174
|
+
ts.SyntaxKind.CaseBlock,
|
|
1175
|
+
ts.SyntaxKind.ClassDeclaration,
|
|
1176
|
+
ts.SyntaxKind.ClassExpression
|
|
1177
|
+
]);
|
|
1178
|
+
var opensScope = (n) => FUNCTION_SCOPE.has(n.kind) || BLOCK_SCOPE.has(n.kind);
|
|
1179
|
+
var declKind = (list) => (list.flags & ts.NodeFlags.Const) !== 0 ? "const" : (list.flags & ts.NodeFlags.Let) !== 0 ? "let" : "var";
|
|
1180
|
+
function declareName(name, kind, decl, into) {
|
|
1181
|
+
if (ts.isIdentifier(name)) {
|
|
1182
|
+
into({ name: name.text, decl, kind });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
for (const el of name.elements) {
|
|
1186
|
+
if (ts.isOmittedExpression(el)) continue;
|
|
1187
|
+
declareName(el.name, kind, el, into);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function buildScopes(sourceFile) {
|
|
1191
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
1192
|
+
const scopeFor = (node, parent) => {
|
|
1193
|
+
const existing = byNode.get(node);
|
|
1194
|
+
if (existing) return existing;
|
|
1195
|
+
const s = { parent, isFunctionScope: FUNCTION_SCOPE.has(node.kind), bindings: /* @__PURE__ */ new Map() };
|
|
1196
|
+
byNode.set(node, s);
|
|
1197
|
+
return s;
|
|
1198
|
+
};
|
|
1199
|
+
const functionScopeOf = (s) => {
|
|
1200
|
+
let cur = s;
|
|
1201
|
+
while (!cur.isFunctionScope && cur.parent) cur = cur.parent;
|
|
1202
|
+
return cur;
|
|
1203
|
+
};
|
|
1204
|
+
const add = (s, b) => {
|
|
1205
|
+
if (!s.bindings.has(b.name)) s.bindings.set(b.name, b);
|
|
1206
|
+
};
|
|
1207
|
+
const visit = (node, scope) => {
|
|
1208
|
+
const inner = opensScope(node) && node !== sourceFile ? scopeFor(node, scope) : scope;
|
|
1209
|
+
if (ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent)) {
|
|
1210
|
+
const kind = declKind(node.parent);
|
|
1211
|
+
const target = kind === "var" ? functionScopeOf(scope) : scope;
|
|
1212
|
+
declareName(node.name, kind, node, (b) => add(target, b));
|
|
1213
|
+
} else if (ts.isParameter(node)) {
|
|
1214
|
+
declareName(node.name, "param", node, (b) => add(scope, b));
|
|
1215
|
+
} else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1216
|
+
add(functionScopeOf(scope), { name: node.name.text, decl: node, kind: "function" });
|
|
1217
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
1218
|
+
add(scope, { name: node.name.text, decl: node, kind: "class" });
|
|
1219
|
+
} else if (ts.isClassExpression(node) && node.name) {
|
|
1220
|
+
add(inner, { name: node.name.text, decl: node, kind: "class" });
|
|
1221
|
+
} else if (ts.isFunctionExpression(node) && node.name) {
|
|
1222
|
+
add(inner, { name: node.name.text, decl: node, kind: "function" });
|
|
1223
|
+
} else if (ts.isImportClause(node) && node.name) {
|
|
1224
|
+
add(scope, { name: node.name.text, decl: node, kind: "import" });
|
|
1225
|
+
} else if (ts.isNamespaceImport(node) || ts.isImportSpecifier(node)) {
|
|
1226
|
+
add(scope, { name: node.name.text, decl: node, kind: "import" });
|
|
1227
|
+
} else if (ts.isCatchClause(node) && node.variableDeclaration) {
|
|
1228
|
+
declareName(node.variableDeclaration.name, "catch", node.variableDeclaration, (b) => add(inner, b));
|
|
1229
|
+
} else if (ts.isEnumDeclaration(node) && node.name) {
|
|
1230
|
+
add(scope, { name: node.name.text, decl: node, kind: "class" });
|
|
1231
|
+
}
|
|
1232
|
+
ts.forEachChild(node, (child) => visit(child, inner));
|
|
1233
|
+
};
|
|
1234
|
+
const root = scopeFor(sourceFile, null);
|
|
1235
|
+
ts.forEachChild(sourceFile, (child) => visit(child, root));
|
|
1236
|
+
return { byNode, sourceFile, reassigned: /* @__PURE__ */ new Map() };
|
|
1237
|
+
}
|
|
1238
|
+
function scopeAt(table, node) {
|
|
1239
|
+
let cur = node;
|
|
1240
|
+
while (cur) {
|
|
1241
|
+
const s = table.byNode.get(cur);
|
|
1242
|
+
if (s) return s;
|
|
1243
|
+
cur = cur.parent;
|
|
1244
|
+
}
|
|
1245
|
+
return table.byNode.get(table.sourceFile);
|
|
1246
|
+
}
|
|
1247
|
+
function isReferencePosition(id) {
|
|
1248
|
+
const p = id.parent;
|
|
1249
|
+
if (!p) return true;
|
|
1250
|
+
if (ts.isPropertyAccessExpression(p) && p.name === id) return false;
|
|
1251
|
+
if (ts.isQualifiedName(p) && p.right === id) return false;
|
|
1252
|
+
if ((ts.isPropertyAssignment(p) || ts.isPropertySignature(p) || ts.isEnumMember(p)) && p.name === id) return false;
|
|
1253
|
+
if (ts.isMethodDeclaration(p) && p.name === id) return false;
|
|
1254
|
+
if (ts.isBindingElement(p) && p.propertyName === id) return false;
|
|
1255
|
+
if (ts.isImportSpecifier(p) && p.propertyName === id) return false;
|
|
1256
|
+
if (ts.isExportSpecifier(p) && p.propertyName === id) return false;
|
|
1257
|
+
if (ts.isLabeledStatement(p) && p.label === id) return false;
|
|
1258
|
+
if (ts.isBreakOrContinueStatement(p) && p.label === id) return false;
|
|
1259
|
+
const named = p;
|
|
1260
|
+
if (named.name === id && !ts.isCallExpression(p) && !ts.isNewExpression(p)) return false;
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
function resolveBinding(table, id) {
|
|
1264
|
+
if (!isReferencePosition(id)) return null;
|
|
1265
|
+
let scope = scopeAt(table, id);
|
|
1266
|
+
while (scope) {
|
|
1267
|
+
const b = scope.bindings.get(id.text);
|
|
1268
|
+
if (b) return b.decl;
|
|
1269
|
+
scope = scope.parent;
|
|
1270
|
+
}
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
var ASSIGN_OPS = /* @__PURE__ */ new Set([
|
|
1274
|
+
ts.SyntaxKind.EqualsToken,
|
|
1275
|
+
ts.SyntaxKind.PlusEqualsToken,
|
|
1276
|
+
ts.SyntaxKind.MinusEqualsToken,
|
|
1277
|
+
ts.SyntaxKind.AsteriskEqualsToken,
|
|
1278
|
+
ts.SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
1279
|
+
ts.SyntaxKind.SlashEqualsToken,
|
|
1280
|
+
ts.SyntaxKind.PercentEqualsToken,
|
|
1281
|
+
ts.SyntaxKind.LessThanLessThanEqualsToken,
|
|
1282
|
+
ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
1283
|
+
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
1284
|
+
ts.SyntaxKind.AmpersandEqualsToken,
|
|
1285
|
+
ts.SyntaxKind.BarEqualsToken,
|
|
1286
|
+
ts.SyntaxKind.CaretEqualsToken,
|
|
1287
|
+
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
1288
|
+
ts.SyntaxKind.BarBarEqualsToken,
|
|
1289
|
+
ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
1290
|
+
]);
|
|
1291
|
+
function isReassigned(table, decl) {
|
|
1292
|
+
const memo = table.reassigned.get(decl);
|
|
1293
|
+
if (memo !== void 0) return memo;
|
|
1294
|
+
if (ts.isVariableDeclaration(decl) && ts.isVariableDeclarationList(decl.parent) && declKind(decl.parent) === "const") {
|
|
1295
|
+
table.reassigned.set(decl, false);
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
let found = false;
|
|
1299
|
+
const check = (target) => {
|
|
1300
|
+
if (found || !ts.isIdentifier(target)) return;
|
|
1301
|
+
if (resolveBinding(table, target) === decl) found = true;
|
|
1302
|
+
};
|
|
1303
|
+
const visit = (n) => {
|
|
1304
|
+
if (found) return;
|
|
1305
|
+
if (ts.isBinaryExpression(n) && ASSIGN_OPS.has(n.operatorToken.kind)) check(n.left);
|
|
1306
|
+
else if ((ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
1307
|
+
check(n.operand);
|
|
1308
|
+
} else if ((ts.isForInStatement(n) || ts.isForOfStatement(n)) && !ts.isVariableDeclarationList(n.initializer)) {
|
|
1309
|
+
check(n.initializer);
|
|
1310
|
+
}
|
|
1311
|
+
ts.forEachChild(n, visit);
|
|
1312
|
+
};
|
|
1313
|
+
visit(table.sourceFile);
|
|
1314
|
+
table.reassigned.set(decl, found);
|
|
1315
|
+
return found;
|
|
1316
|
+
}
|
|
1317
|
+
var MAX_ALIAS_DEPTH = 8;
|
|
1318
|
+
function resolveThroughAliases(table, id) {
|
|
1319
|
+
let decl = resolveBinding(table, id);
|
|
1320
|
+
if (!decl) return null;
|
|
1321
|
+
const seen = /* @__PURE__ */ new Set([decl]);
|
|
1322
|
+
for (let depth = 0; depth < MAX_ALIAS_DEPTH; depth++) {
|
|
1323
|
+
if (!ts.isVariableDeclaration(decl) || !decl.initializer || !ts.isIdentifier(decl.initializer)) return decl;
|
|
1324
|
+
const target = resolveBinding(table, decl.initializer);
|
|
1325
|
+
if (!target || seen.has(target)) return decl;
|
|
1326
|
+
if (isReassigned(table, decl) || isReassigned(table, target)) return decl;
|
|
1327
|
+
seen.add(target);
|
|
1328
|
+
decl = target;
|
|
1329
|
+
}
|
|
1330
|
+
return decl;
|
|
1331
|
+
}
|
|
1332
|
+
function namesInScopeAt(table, node) {
|
|
1333
|
+
const out = /* @__PURE__ */ new Set();
|
|
1334
|
+
let scope = scopeAt(table, node);
|
|
1335
|
+
while (scope) {
|
|
1336
|
+
for (const name of scope.bindings.keys()) out.add(name);
|
|
1337
|
+
scope = scope.parent;
|
|
1338
|
+
}
|
|
1339
|
+
return out;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/source-analysis/sampling.ts
|
|
1343
|
+
var SAMPLING_ALGORITHM = "line-first-rr-v1";
|
|
1344
|
+
var mutantKey = (m) => `${m.file}:${m.startLine}:${m.startColumn}-${m.endLine}:${m.endColumn}:${m.mutatorName}:${m.replacement ?? ""}`;
|
|
1345
|
+
function spanContains(outer, inner) {
|
|
1346
|
+
if (outer.file !== inner.file) return false;
|
|
1347
|
+
const startsAfter = inner.startLine > outer.startLine || inner.startLine === outer.startLine && inner.startColumn >= outer.startColumn;
|
|
1348
|
+
const endsBefore = inner.endLine < outer.endLine || inner.endLine === outer.endLine && inner.endColumn <= outer.endColumn;
|
|
1349
|
+
return startsAfter && endsBefore;
|
|
1350
|
+
}
|
|
1351
|
+
function containmentClosures(mutants) {
|
|
1352
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const m of mutants) {
|
|
1354
|
+
const arr = byFile.get(m.file);
|
|
1355
|
+
if (arr) arr.push(m);
|
|
1356
|
+
else byFile.set(m.file, [m]);
|
|
1357
|
+
}
|
|
1358
|
+
const out = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const arr of byFile.values()) {
|
|
1360
|
+
for (const outer of arr) {
|
|
1361
|
+
out.set(mutantKey(outer), arr.filter((inner) => spanContains(outer, inner)));
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
var lineKey2 = (m) => `${m.file}:${m.startLine}`;
|
|
1367
|
+
var rank = (seed, ...parts) => sha256(`${seed}|${parts.join("|")}`);
|
|
1368
|
+
function lineFirstSample(mutants, cap, seed) {
|
|
1369
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
1370
|
+
for (const m of mutants) {
|
|
1371
|
+
const k = lineKey2(m);
|
|
1372
|
+
const arr = byLine.get(k);
|
|
1373
|
+
if (arr) arr.push(m);
|
|
1374
|
+
else byLine.set(k, [m]);
|
|
1375
|
+
}
|
|
1376
|
+
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1377
|
+
for (const [k, arr] of byLine) {
|
|
1378
|
+
arr.sort((a, b) => cmp(rank(seed, k, mutantKey(a)), rank(seed, k, mutantKey(b))));
|
|
1379
|
+
}
|
|
1380
|
+
const lines = [...byLine.keys()].sort((a, b) => cmp(rank(seed, a), rank(seed, b)));
|
|
1381
|
+
const closures = containmentClosures(mutants);
|
|
1382
|
+
const sampled = [];
|
|
1383
|
+
const executedByKey = /* @__PURE__ */ new Map();
|
|
1384
|
+
const probed = /* @__PURE__ */ new Set();
|
|
1385
|
+
let skippedOversizeClosures = 0;
|
|
1386
|
+
if (cap > 0) {
|
|
1387
|
+
for (let round = 0; executedByKey.size < cap; round++) {
|
|
1388
|
+
let tookAny = false;
|
|
1389
|
+
let sawUnpicked = false;
|
|
1390
|
+
for (const k of lines) {
|
|
1391
|
+
if (executedByKey.size >= cap) break;
|
|
1392
|
+
const arr = byLine.get(k);
|
|
1393
|
+
if (round >= arr.length) continue;
|
|
1394
|
+
sawUnpicked = true;
|
|
1395
|
+
const pick = arr[round];
|
|
1396
|
+
const closure = closures.get(mutantKey(pick)) ?? [pick];
|
|
1397
|
+
const fresh = closure.filter((m) => !executedByKey.has(mutantKey(m)));
|
|
1398
|
+
if (fresh.length > cap - executedByKey.size) {
|
|
1399
|
+
skippedOversizeClosures++;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
sampled.push(pick);
|
|
1403
|
+
for (const m of fresh) executedByKey.set(mutantKey(m), m);
|
|
1404
|
+
for (const m of closure) probed.add(lineKey2(m));
|
|
1405
|
+
tookAny = true;
|
|
1406
|
+
}
|
|
1407
|
+
if (!tookAny && !sawUnpicked) break;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
algorithm: SAMPLING_ALGORITHM,
|
|
1412
|
+
seed,
|
|
1413
|
+
cap,
|
|
1414
|
+
eligible: mutants.length,
|
|
1415
|
+
sampled,
|
|
1416
|
+
executed: [...executedByKey.values()],
|
|
1417
|
+
linesEligible: byLine.size,
|
|
1418
|
+
linesProbed: probed.size,
|
|
1419
|
+
skippedOversizeClosures
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function sampleToMutateTargets(plan, subdir) {
|
|
1423
|
+
const prefix = subdir ? `${subdir}/` : "";
|
|
1424
|
+
return plan.sampled.map((m) => {
|
|
1425
|
+
const rel = m.file.startsWith(prefix) ? m.file.slice(prefix.length) : m.file;
|
|
1426
|
+
return `${escapeMutatePath(rel)}:${m.startLine}:${m.startColumn}-${m.endLine}:${m.endColumn}`;
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// src/source-analysis/deterministic-operators.ts
|
|
1431
|
+
var DETERMINISTIC_CATEGORIES = [
|
|
1432
|
+
"missing-await",
|
|
1433
|
+
"exception-swallow",
|
|
1434
|
+
"argument-order",
|
|
1435
|
+
"off-by-one",
|
|
1436
|
+
"wrong-variable",
|
|
1437
|
+
"wrong-constant"
|
|
1438
|
+
];
|
|
1439
|
+
var DEFAULT_MAX_PER_CATEGORY = 25;
|
|
1440
|
+
function lineOf2(source, offset) {
|
|
1441
|
+
let line = 1;
|
|
1442
|
+
const end = Math.min(offset, source.length);
|
|
1443
|
+
for (let i = 0; i < end; i++) {
|
|
1444
|
+
const c = source[i];
|
|
1445
|
+
if (c === "\r") {
|
|
1446
|
+
if (source[i + 1] === "\n") i++;
|
|
1447
|
+
line++;
|
|
1448
|
+
} else if (c === "\n" || c === "\u2028" || c === "\u2029") line++;
|
|
1449
|
+
}
|
|
1450
|
+
return line;
|
|
1451
|
+
}
|
|
1452
|
+
function editDistance(a, b, cap = 3) {
|
|
1453
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
1454
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1455
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1456
|
+
const cur = [i];
|
|
1457
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1458
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1459
|
+
}
|
|
1460
|
+
prev = cur;
|
|
1461
|
+
}
|
|
1462
|
+
return prev[b.length];
|
|
1463
|
+
}
|
|
1464
|
+
function isConfusableIdentifier(a, b) {
|
|
1465
|
+
if (a === b) return false;
|
|
1466
|
+
if (a.length < 3 || b.length < 3) return false;
|
|
1467
|
+
const d = editDistance(a, b);
|
|
1468
|
+
return d > 0 && d <= Math.max(1, Math.floor(Math.min(a.length, b.length) / 4) + 1);
|
|
1469
|
+
}
|
|
1470
|
+
function plausibleConstants(literal) {
|
|
1471
|
+
if (literal === "true") return ["false"];
|
|
1472
|
+
if (literal === "false") return ["true"];
|
|
1473
|
+
const n = Number(literal);
|
|
1474
|
+
if (!Number.isFinite(n)) return [];
|
|
1475
|
+
const out = /* @__PURE__ */ new Set();
|
|
1476
|
+
out.add(String(n + 1));
|
|
1477
|
+
out.add(String(n - 1));
|
|
1478
|
+
if (n !== 0) out.add(String(-n));
|
|
1479
|
+
if (n === 1e3) out.add("60");
|
|
1480
|
+
if (n === 60) out.add("1000");
|
|
1481
|
+
if (n === 24) out.add("12");
|
|
1482
|
+
if (n === 12) out.add("24");
|
|
1483
|
+
out.delete(literal);
|
|
1484
|
+
return [...out];
|
|
1485
|
+
}
|
|
1486
|
+
var BOUNDARY_FLIP = {
|
|
1487
|
+
[ts.SyntaxKind.LessThanToken]: "<=",
|
|
1488
|
+
[ts.SyntaxKind.LessThanEqualsToken]: "<",
|
|
1489
|
+
[ts.SyntaxKind.GreaterThanToken]: ">=",
|
|
1490
|
+
[ts.SyntaxKind.GreaterThanEqualsToken]: ">"
|
|
1491
|
+
};
|
|
1492
|
+
function deterministicRecipes(file, source, opts = {}) {
|
|
1493
|
+
const parsed = parseSource(file, source);
|
|
1494
|
+
if (!parsed.parseOk) return [];
|
|
1495
|
+
const sf = parsed.sourceFile;
|
|
1496
|
+
const cap = opts.maxPerCategory ?? DEFAULT_MAX_PER_CATEGORY;
|
|
1497
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1498
|
+
const out = [];
|
|
1499
|
+
const emit = (start, end, replacement, category) => {
|
|
1500
|
+
if ((counts.get(category) ?? 0) >= cap) return;
|
|
1501
|
+
if (start >= end || end > source.length) return;
|
|
1502
|
+
if (source.slice(start, end) === replacement) return;
|
|
1503
|
+
if (opts.changedLines) {
|
|
1504
|
+
const from = lineOf2(source, start);
|
|
1505
|
+
const to = lineOf2(source, end);
|
|
1506
|
+
let hit = false;
|
|
1507
|
+
for (let l = from; l <= to && !hit; l++) if (opts.changedLines.has(l)) hit = true;
|
|
1508
|
+
if (!hit) return;
|
|
1509
|
+
}
|
|
1510
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
1511
|
+
out.push(recipeFromOffsets(file, source, start, end, replacement, category));
|
|
1512
|
+
};
|
|
1513
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
1514
|
+
const collect = (node) => {
|
|
1515
|
+
if (ts.isIdentifier(node)) identifiers.add(node.text);
|
|
1516
|
+
ts.forEachChild(node, collect);
|
|
1517
|
+
};
|
|
1518
|
+
collect(sf);
|
|
1519
|
+
const scopes = buildScopes(sf);
|
|
1520
|
+
const visit = (node) => {
|
|
1521
|
+
if (ts.isAwaitExpression(node)) {
|
|
1522
|
+
const start = node.getStart(sf);
|
|
1523
|
+
const inner = node.expression.getStart(sf);
|
|
1524
|
+
emit(start, inner, "", "missing-await");
|
|
1525
|
+
}
|
|
1526
|
+
if (ts.isThrowStatement(node)) {
|
|
1527
|
+
const start = node.getStart(sf);
|
|
1528
|
+
const inner = node.expression.getStart(sf);
|
|
1529
|
+
emit(start, inner, "", "exception-swallow");
|
|
1530
|
+
}
|
|
1531
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && node.arguments && node.arguments.length >= 2) {
|
|
1532
|
+
for (let i = 0; i + 1 < node.arguments.length; i++) {
|
|
1533
|
+
const a = node.arguments[i];
|
|
1534
|
+
const b = node.arguments[i + 1];
|
|
1535
|
+
const aText = source.slice(a.getStart(sf), a.getEnd());
|
|
1536
|
+
const bText = source.slice(b.getStart(sf), b.getEnd());
|
|
1537
|
+
if (aText === bText) continue;
|
|
1538
|
+
const sep2 = source.slice(a.getEnd(), b.getStart(sf));
|
|
1539
|
+
emit(a.getStart(sf), b.getEnd(), `${bText}${sep2}${aText}`, "argument-order");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (ts.isBinaryExpression(node)) {
|
|
1543
|
+
const flip = BOUNDARY_FLIP[node.operatorToken.kind];
|
|
1544
|
+
if (flip) emit(node.operatorToken.getStart(sf), node.operatorToken.getEnd(), flip, "off-by-one");
|
|
1545
|
+
}
|
|
1546
|
+
if (ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1547
|
+
const start = node.getStart(sf);
|
|
1548
|
+
const end = node.getEnd();
|
|
1549
|
+
const text = source.slice(start, end);
|
|
1550
|
+
const options = plausibleConstants(text);
|
|
1551
|
+
if (options.length > 0) {
|
|
1552
|
+
const n = Number(text);
|
|
1553
|
+
const offByOne = Number.isFinite(n) ? options.filter((o) => Number(o) === n + 1 || Number(o) === n - 1) : [];
|
|
1554
|
+
const others = options.filter((o) => !offByOne.includes(o));
|
|
1555
|
+
if (offByOne.length > 0) emit(start, end, offByOne[0], "off-by-one");
|
|
1556
|
+
if (others.length > 0) emit(start, end, others[0], "wrong-constant");
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node.parent) && !ts.isPropertyAssignment(node.parent)) {
|
|
1560
|
+
const name = node.text;
|
|
1561
|
+
const visible = namesInScopeAt(scopes, node);
|
|
1562
|
+
for (const other of identifiers) {
|
|
1563
|
+
if (visible.has(other) && isConfusableIdentifier(name, other)) {
|
|
1564
|
+
emit(node.getStart(sf), node.getEnd(), other, "wrong-variable");
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
ts.forEachChild(node, visit);
|
|
1570
|
+
};
|
|
1571
|
+
visit(sf);
|
|
1572
|
+
return out;
|
|
1573
|
+
}
|
|
1574
|
+
function categoryCounts(recipes) {
|
|
1575
|
+
const out = {};
|
|
1576
|
+
for (const c of DETERMINISTIC_CATEGORIES) out[c] = 0;
|
|
1577
|
+
for (const r of recipes) if (r.category) out[r.category] = (out[r.category] ?? 0) + 1;
|
|
1578
|
+
return out;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/source-analysis/production-operators.ts
|
|
1582
|
+
var PRODUCTION_OPERATOR_VERSION = "production-operators-v2";
|
|
1583
|
+
var PRODUCTION_OPERATOR_CATEGORIES = [
|
|
1584
|
+
"statement-deletion",
|
|
1585
|
+
"return-deletion",
|
|
1586
|
+
"control-flow-deletion",
|
|
1587
|
+
"argument-omission",
|
|
1588
|
+
"argument-order",
|
|
1589
|
+
"argument-replacement",
|
|
1590
|
+
"identifier-replacement",
|
|
1591
|
+
"property-substitution",
|
|
1592
|
+
"assignment-operator",
|
|
1593
|
+
"assignment-rhs",
|
|
1594
|
+
"missing-await",
|
|
1595
|
+
"nullish-fallback",
|
|
1596
|
+
"optional-chain-removal",
|
|
1597
|
+
"call-chain-omission",
|
|
1598
|
+
"parameter-default-removal",
|
|
1599
|
+
"class-field-initializer-removal"
|
|
1600
|
+
];
|
|
1601
|
+
var DEFAULT_MAX_PER_CATEGORY2 = 20;
|
|
1602
|
+
function lineOf3(source, offset) {
|
|
1603
|
+
let line = 1;
|
|
1604
|
+
const end = Math.min(offset, source.length);
|
|
1605
|
+
for (let i = 0; i < end; i++) {
|
|
1606
|
+
if (source[i] === "\r") {
|
|
1607
|
+
if (source[i + 1] === "\n") i++;
|
|
1608
|
+
line++;
|
|
1609
|
+
} else if (source[i] === "\n" || source[i] === "\u2028" || source[i] === "\u2029") {
|
|
1610
|
+
line++;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return line;
|
|
1614
|
+
}
|
|
1615
|
+
function intersectsChanged(source, start, end, changed) {
|
|
1616
|
+
if (!changed) return true;
|
|
1617
|
+
for (let line = lineOf3(source, start); line <= lineOf3(source, end); line++) {
|
|
1618
|
+
if (changed.has(line)) return true;
|
|
1619
|
+
}
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
function syntaxKindTag(node) {
|
|
1623
|
+
if (!node) return null;
|
|
1624
|
+
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return "string";
|
|
1625
|
+
if (ts.isNumericLiteral(node)) return "number";
|
|
1626
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return "boolean";
|
|
1627
|
+
if (ts.isArrayLiteralExpression(node)) return "array";
|
|
1628
|
+
if (ts.isObjectLiteralExpression(node)) return "object";
|
|
1629
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return "function";
|
|
1630
|
+
if (ts.isIdentifier(node)) return "identifier";
|
|
1631
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) return "call";
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
function declarationTypeTags(sf) {
|
|
1635
|
+
const out = /* @__PURE__ */ new Map();
|
|
1636
|
+
const add = (name, tag) => {
|
|
1637
|
+
if (!tag) return;
|
|
1638
|
+
const tags = out.get(name) ?? /* @__PURE__ */ new Set();
|
|
1639
|
+
tags.add(tag);
|
|
1640
|
+
out.set(name, tags);
|
|
1641
|
+
};
|
|
1642
|
+
const visit = (node) => {
|
|
1643
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1644
|
+
add(node.name.text, node.type ? `type:${node.type.getText(sf)}` : syntaxKindTag(node.initializer));
|
|
1645
|
+
} else if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
|
|
1646
|
+
add(node.name.text, node.type ? `type:${node.type.getText(sf)}` : syntaxKindTag(node.initializer));
|
|
1647
|
+
} else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1648
|
+
add(node.name.text, "function");
|
|
1649
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
1650
|
+
add(node.name.text, "class");
|
|
1651
|
+
}
|
|
1652
|
+
ts.forEachChild(node, visit);
|
|
1653
|
+
};
|
|
1654
|
+
visit(sf);
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1657
|
+
function typeCompatible(a, b, tags) {
|
|
1658
|
+
const aa = tags.get(a);
|
|
1659
|
+
const bb = tags.get(b);
|
|
1660
|
+
if (!aa || !bb || aa.size === 0 || bb.size === 0) return false;
|
|
1661
|
+
for (const tag of aa) if (bb.has(tag)) return true;
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
function isReferenceIdentifier(node) {
|
|
1665
|
+
const p = node.parent;
|
|
1666
|
+
if (ts.isVariableDeclaration(p) && p.name === node || ts.isBindingElement(p) && p.name === node || ts.isParameter(p) && p.name === node || ts.isFunctionDeclaration(p) && p.name === node || ts.isFunctionExpression(p) && p.name === node || ts.isClassDeclaration(p) && p.name === node || ts.isClassExpression(p) && p.name === node || ts.isPropertyAccessExpression(p) && p.name === node || ts.isPropertyAssignment(p) && p.name === node || ts.isPropertyDeclaration(p) && p.name === node || ts.isMethodDeclaration(p) && p.name === node || (ts.isImportSpecifier(p) || ts.isExportSpecifier(p))) {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
function referenceIdentifierSpans(sf) {
|
|
1672
|
+
const spans = /* @__PURE__ */ new Set();
|
|
1673
|
+
const visit = (node) => {
|
|
1674
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
|
|
1675
|
+
spans.add(`${node.getStart(sf)}:${node.getEnd()}`);
|
|
1676
|
+
}
|
|
1677
|
+
ts.forEachChild(node, visit);
|
|
1678
|
+
};
|
|
1679
|
+
visit(sf);
|
|
1680
|
+
return spans;
|
|
1681
|
+
}
|
|
1682
|
+
var ASSIGNMENT_REPLACEMENTS = /* @__PURE__ */ new Map([
|
|
1683
|
+
[ts.SyntaxKind.EqualsToken, "+="],
|
|
1684
|
+
[ts.SyntaxKind.PlusEqualsToken, "="],
|
|
1685
|
+
[ts.SyntaxKind.MinusEqualsToken, "="],
|
|
1686
|
+
[ts.SyntaxKind.AsteriskEqualsToken, "="],
|
|
1687
|
+
[ts.SyntaxKind.SlashEqualsToken, "="],
|
|
1688
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, "??="],
|
|
1689
|
+
[ts.SyntaxKind.BarBarEqualsToken, "??="],
|
|
1690
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, "="]
|
|
1691
|
+
]);
|
|
1692
|
+
function argumentRemovalSpan(args, index, sf) {
|
|
1693
|
+
const arg = args[index];
|
|
1694
|
+
if (args.length === 1) return [arg.getStart(sf), arg.getEnd()];
|
|
1695
|
+
if (index < args.length - 1) return [arg.getStart(sf), args[index + 1].getStart(sf)];
|
|
1696
|
+
return [args[index - 1].getEnd(), arg.getEnd()];
|
|
1697
|
+
}
|
|
1698
|
+
function propertyGroups(sf, source) {
|
|
1699
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1700
|
+
const visit = (node) => {
|
|
1701
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1702
|
+
const receiver = source.slice(node.expression.getStart(sf), node.expression.getEnd());
|
|
1703
|
+
const names = groups.get(receiver) ?? /* @__PURE__ */ new Set();
|
|
1704
|
+
names.add(node.name.text);
|
|
1705
|
+
groups.set(receiver, names);
|
|
1706
|
+
}
|
|
1707
|
+
ts.forEachChild(node, visit);
|
|
1708
|
+
};
|
|
1709
|
+
visit(sf);
|
|
1710
|
+
return groups;
|
|
1711
|
+
}
|
|
1712
|
+
function productionRecipes(file, source, options = {}) {
|
|
1713
|
+
const parsed = parseSource(file, source);
|
|
1714
|
+
if (!parsed.parseOk) return [];
|
|
1715
|
+
const sf = parsed.sourceFile;
|
|
1716
|
+
const scopes = buildScopes(sf);
|
|
1717
|
+
const tags = declarationTypeTags(sf);
|
|
1718
|
+
const properties = propertyGroups(sf, source);
|
|
1719
|
+
const referenceSpans = referenceIdentifierSpans(sf);
|
|
1720
|
+
const cap = options.maxPerCategory ?? DEFAULT_MAX_PER_CATEGORY2;
|
|
1721
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1722
|
+
const recipes = /* @__PURE__ */ new Map();
|
|
1723
|
+
const emit = (start, end, replacement, category) => {
|
|
1724
|
+
if ((counts.get(category) ?? 0) >= cap) return;
|
|
1725
|
+
if (start < 0 || start >= end || end > source.length) return;
|
|
1726
|
+
if (source.slice(start, end) === replacement) return;
|
|
1727
|
+
if (!intersectsChanged(source, start, end, options.changedLines)) return;
|
|
1728
|
+
const recipe = recipeFromOffsets(file, source, start, end, replacement, category);
|
|
1729
|
+
const mutated = source.slice(0, start) + replacement + source.slice(end);
|
|
1730
|
+
if (!parseSource(file, mutated).parseOk) return;
|
|
1731
|
+
if (recipes.has(recipe.id)) return;
|
|
1732
|
+
recipes.set(recipe.id, recipe);
|
|
1733
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
1734
|
+
};
|
|
1735
|
+
if (options.includeControl !== false) {
|
|
1736
|
+
for (const recipe of deterministicRecipes(file, source, {
|
|
1737
|
+
changedLines: options.changedLines,
|
|
1738
|
+
maxPerCategory: cap
|
|
1739
|
+
})) {
|
|
1740
|
+
const category = recipe.category ?? "unknown";
|
|
1741
|
+
if (category === "wrong-variable" && !referenceSpans.has(`${recipe.startOffset}:${recipe.endOffset}`)) {
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if ((counts.get(category) ?? 0) >= cap) continue;
|
|
1745
|
+
recipes.set(recipe.id, recipe);
|
|
1746
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const visit = (node) => {
|
|
1750
|
+
if (ts.isExpressionStatement(node) && (ts.isCallExpression(node.expression) || ts.isAwaitExpression(node.expression) && ts.isCallExpression(node.expression.expression))) {
|
|
1751
|
+
emit(node.getStart(sf), node.getEnd(), ";", "statement-deletion");
|
|
1752
|
+
}
|
|
1753
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
1754
|
+
emit(node.expression.getStart(sf), node.expression.getEnd(), "undefined", "return-deletion");
|
|
1755
|
+
}
|
|
1756
|
+
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
|
|
1757
|
+
emit(node.getStart(sf), node.getEnd(), ";", "control-flow-deletion");
|
|
1758
|
+
}
|
|
1759
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && node.arguments) {
|
|
1760
|
+
for (let index = 0; index < node.arguments.length; index++) {
|
|
1761
|
+
const [start, end] = argumentRemovalSpan(node.arguments, index, sf);
|
|
1762
|
+
emit(start, end, "", "argument-omission");
|
|
1763
|
+
const arg = node.arguments[index];
|
|
1764
|
+
if (ts.isIdentifier(arg)) {
|
|
1765
|
+
const visible = namesInScopeAt(scopes, arg);
|
|
1766
|
+
for (const candidate of visible) {
|
|
1767
|
+
if (candidate !== arg.text && typeCompatible(arg.text, candidate, tags)) {
|
|
1768
|
+
emit(arg.getStart(sf), arg.getEnd(), candidate, "argument-replacement");
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && (ts.isCallExpression(node.expression.expression) || ts.isPropertyAccessExpression(node.expression.expression))) {
|
|
1775
|
+
const receiver = node.expression.expression;
|
|
1776
|
+
emit(node.getStart(sf), node.getEnd(), source.slice(receiver.getStart(sf), receiver.getEnd()), "call-chain-omission");
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
|
|
1780
|
+
const visible = namesInScopeAt(scopes, node);
|
|
1781
|
+
for (const candidate of visible) {
|
|
1782
|
+
if (candidate !== node.text && typeCompatible(node.text, candidate, tags)) {
|
|
1783
|
+
emit(node.getStart(sf), node.getEnd(), candidate, "identifier-replacement");
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1789
|
+
const receiver = source.slice(node.expression.getStart(sf), node.expression.getEnd());
|
|
1790
|
+
const candidates = properties.get(receiver);
|
|
1791
|
+
if (candidates) {
|
|
1792
|
+
for (const candidate of candidates) {
|
|
1793
|
+
if (candidate !== node.name.text) {
|
|
1794
|
+
emit(node.name.getStart(sf), node.name.getEnd(), candidate, "property-substitution");
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (node.questionDotToken) {
|
|
1800
|
+
emit(node.questionDotToken.getStart(sf), node.questionDotToken.getEnd(), ".", "optional-chain-removal");
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (ts.isCallExpression(node) && node.questionDotToken) {
|
|
1804
|
+
emit(node.questionDotToken.getStart(sf), node.questionDotToken.getEnd(), "", "optional-chain-removal");
|
|
1805
|
+
}
|
|
1806
|
+
if (ts.isBinaryExpression(node)) {
|
|
1807
|
+
const assignment = ASSIGNMENT_REPLACEMENTS.get(node.operatorToken.kind);
|
|
1808
|
+
if (assignment) {
|
|
1809
|
+
emit(node.operatorToken.getStart(sf), node.operatorToken.getEnd(), assignment, "assignment-operator");
|
|
1810
|
+
if (ts.isIdentifier(node.right)) {
|
|
1811
|
+
const visible = namesInScopeAt(scopes, node.right);
|
|
1812
|
+
for (const candidate of visible) {
|
|
1813
|
+
if (candidate !== node.right.text && typeCompatible(node.right.text, candidate, tags)) {
|
|
1814
|
+
emit(node.right.getStart(sf), node.right.getEnd(), candidate, "assignment-rhs");
|
|
1815
|
+
break;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1821
|
+
emit(node.getStart(sf), node.getEnd(), source.slice(node.left.getStart(sf), node.left.getEnd()), "nullish-fallback");
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (ts.isParameter(node) && node.initializer) {
|
|
1825
|
+
emit(node.name.getEnd(), node.initializer.getEnd(), "", "parameter-default-removal");
|
|
1826
|
+
}
|
|
1827
|
+
if (ts.isPropertyDeclaration(node) && node.initializer) {
|
|
1828
|
+
emit(node.name.getEnd(), node.initializer.getEnd(), "", "class-field-initializer-removal");
|
|
1829
|
+
}
|
|
1830
|
+
ts.forEachChild(node, visit);
|
|
1831
|
+
};
|
|
1832
|
+
visit(sf);
|
|
1833
|
+
return [...recipes.values()];
|
|
1834
|
+
}
|
|
1835
|
+
function productionCategoryCounts(recipes) {
|
|
1836
|
+
const counts = {};
|
|
1837
|
+
for (const category of PRODUCTION_OPERATOR_CATEGORIES) counts[category] = 0;
|
|
1838
|
+
for (const recipe of recipes) {
|
|
1839
|
+
const category = recipe.category ?? "unknown";
|
|
1840
|
+
counts[category] = (counts[category] ?? 0) + 1;
|
|
1841
|
+
}
|
|
1842
|
+
return counts;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/source-analysis/classic-operator-inventory.ts
|
|
1846
|
+
var STRYKER_BUILTIN_OPERATORS = [
|
|
1847
|
+
{ id: "ArithmeticOperator", rewrite: "swaps one arithmetic operator for its counterpart: + for -, * for /, % for *" },
|
|
1848
|
+
{ id: "ArrayDeclaration", rewrite: "empties an array literal, or fills an empty one with a single element" },
|
|
1849
|
+
{ id: "ArrowFunction", rewrite: "replaces an arrow function's body with `undefined`" },
|
|
1850
|
+
{ id: "AssignmentOperator", rewrite: "swaps a compound assignment for its counterpart: += for -=, *= for /=, ??= for &&=" },
|
|
1851
|
+
{ id: "BlockStatement", rewrite: "empties a block, so everything the braces held stops running" },
|
|
1852
|
+
{ id: "BooleanLiteral", rewrite: "flips `true` and `false`, and removes or adds a leading `!`" },
|
|
1853
|
+
{ id: "ConditionalExpression", rewrite: "forces a condition to always-true or always-false, or empties a switch case" },
|
|
1854
|
+
{ id: "EqualityOperator", rewrite: "swaps one comparison for another: == for !=, < for <=, > for >=, and their negations" },
|
|
1855
|
+
{ id: "LogicalOperator", rewrite: "swaps && for ||, || for &&, and ?? for &&" },
|
|
1856
|
+
{ id: "MethodExpression", rewrite: "swaps a built-in method for its opposite: startsWith for endsWith, filter for every, min for max, slice/substr removed" },
|
|
1857
|
+
{ id: "ObjectLiteral", rewrite: "empties an object literal" },
|
|
1858
|
+
{ id: "OptionalChaining", rewrite: "removes the `?.` so the access is no longer guarded" },
|
|
1859
|
+
{ id: "Regex", rewrite: "alters a regular expression literal's pattern" },
|
|
1860
|
+
{ id: "StringLiteral", rewrite: "replaces a string literal with an empty string, or an empty one with a placeholder" },
|
|
1861
|
+
{ id: "UnaryOperator", rewrite: "flips the sign of a unary operator: +x for -x, ~x for x" },
|
|
1862
|
+
{ id: "UpdateOperator", rewrite: "swaps ++ for -- and -- for ++" }
|
|
1863
|
+
];
|
|
1864
|
+
var DETERMINISTIC_PASS_OPERATORS = [
|
|
1865
|
+
// the frozen six-family control inventory
|
|
1866
|
+
{ id: "missing-await", rewrite: "removes an `await`, so the promise is never waited on" },
|
|
1867
|
+
{ id: "exception-swallow", rewrite: "removes a `throw`, leaving its expression evaluated but never raised" },
|
|
1868
|
+
{ id: "argument-order", rewrite: "swaps two ADJACENT arguments of one call, whatever they mean" },
|
|
1869
|
+
{ id: "off-by-one", rewrite: "moves a comparison boundary by one (< for <=, > for >=), or a numeric literal by \xB11" },
|
|
1870
|
+
{ id: "wrong-variable", rewrite: "replaces an identifier with a confusably similar one that is in scope here" },
|
|
1871
|
+
{ id: "wrong-constant", rewrite: "replaces a literal with a plausible neighbour: its negation, 1000 for 60, 24 for 12" },
|
|
1872
|
+
// the production operators
|
|
1873
|
+
{ id: "statement-deletion", rewrite: "replaces a whole call statement with `;`, so it no longer runs" },
|
|
1874
|
+
{ id: "return-deletion", rewrite: "replaces a returned expression with `undefined`" },
|
|
1875
|
+
{ id: "control-flow-deletion", rewrite: "removes a `break` or a `continue`" },
|
|
1876
|
+
{ id: "argument-omission", rewrite: "removes one argument from a call, shifting the rest along" },
|
|
1877
|
+
{ id: "argument-replacement", rewrite: "replaces one argument identifier with another in-scope name of a compatible type" },
|
|
1878
|
+
{ id: "identifier-replacement", rewrite: "replaces any referenced identifier with another in-scope name of a compatible type" },
|
|
1879
|
+
{ id: "property-substitution", rewrite: "replaces a property name with another property seen on the same receiver" },
|
|
1880
|
+
{ id: "assignment-operator", rewrite: "swaps a compound assignment for its counterpart, and = for +=" },
|
|
1881
|
+
{ id: "assignment-rhs", rewrite: "replaces the right-hand side identifier of an assignment with a compatible in-scope name" },
|
|
1882
|
+
{ id: "nullish-fallback", rewrite: "drops the `?? fallback`, leaving only the left-hand side" },
|
|
1883
|
+
{ id: "optional-chain-removal", rewrite: "turns `?.` into `.`, or removes it from a call" },
|
|
1884
|
+
{ id: "call-chain-omission", rewrite: "drops the last call of a chain, returning its receiver instead" },
|
|
1885
|
+
{ id: "parameter-default-removal", rewrite: "removes a parameter's default value" },
|
|
1886
|
+
{ id: "class-field-initializer-removal", rewrite: "removes a class field's initializer" }
|
|
1887
|
+
];
|
|
1888
|
+
function classicOperatorInventory(input) {
|
|
1889
|
+
return input.deterministic ? [...STRYKER_BUILTIN_OPERATORS, ...DETERMINISTIC_PASS_OPERATORS] : [...STRYKER_BUILTIN_OPERATORS];
|
|
1890
|
+
}
|
|
1891
|
+
var DETERMINISTIC_PASS_CATEGORIES = [
|
|
1892
|
+
.../* @__PURE__ */ new Set([...DETERMINISTIC_CATEGORIES, ...PRODUCTION_OPERATOR_CATEGORIES])
|
|
1893
|
+
];
|
|
1894
|
+
export {
|
|
1895
|
+
DESCRIBE_FNS,
|
|
1896
|
+
DETERMINISTIC_CATEGORIES,
|
|
1897
|
+
DETERMINISTIC_PASS_CATEGORIES,
|
|
1898
|
+
DETERMINISTIC_PASS_OPERATORS,
|
|
1899
|
+
PRODUCTION_OPERATOR_CATEGORIES,
|
|
1900
|
+
PRODUCTION_OPERATOR_VERSION,
|
|
1901
|
+
SAMPLING_ALGORITHM,
|
|
1902
|
+
STRYKER_BUILTIN_OPERATORS,
|
|
1903
|
+
TEST_FNS,
|
|
1904
|
+
analyzeReachability,
|
|
1905
|
+
applyRecipe,
|
|
1906
|
+
buildScopes,
|
|
1907
|
+
categoryCounts,
|
|
1908
|
+
classicOperatorInventory,
|
|
1909
|
+
clusterMutants,
|
|
1910
|
+
collectTypeContext,
|
|
1911
|
+
commentInNodeMatches,
|
|
1912
|
+
deterministicRecipes,
|
|
1913
|
+
enclosingNodes,
|
|
1914
|
+
escapeMutatePath,
|
|
1915
|
+
forcedHandlerRecipes,
|
|
1916
|
+
isConfusableIdentifier,
|
|
1917
|
+
isParameterized,
|
|
1918
|
+
isReassigned,
|
|
1919
|
+
lineFirstSample,
|
|
1920
|
+
lineKey,
|
|
1921
|
+
lineOf,
|
|
1922
|
+
moduleExportSurface,
|
|
1923
|
+
mutantKey,
|
|
1924
|
+
namesInScopeAt,
|
|
1925
|
+
offsetOf,
|
|
1926
|
+
parseSource,
|
|
1927
|
+
plausibleConstants,
|
|
1928
|
+
productionCategoryCounts,
|
|
1929
|
+
productionRecipes,
|
|
1930
|
+
recipeFromOffsets,
|
|
1931
|
+
recipeFromSpan,
|
|
1932
|
+
recipeId,
|
|
1933
|
+
sha256 as recipeSha256,
|
|
1934
|
+
resolveBinding,
|
|
1935
|
+
resolveThroughAliases,
|
|
1936
|
+
sampleToMutateTargets,
|
|
1937
|
+
scanErrorHandlers,
|
|
1938
|
+
scanFileHandlers,
|
|
1939
|
+
scanTautologies,
|
|
1940
|
+
strictOffsetOf,
|
|
1941
|
+
stringArg,
|
|
1942
|
+
testCallName,
|
|
1943
|
+
tryRecipeFromSpan,
|
|
1944
|
+
ts
|
|
1945
|
+
};
|