jest-test-lineage-reporter 2.0.1 → 2.0.2

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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Babel Plugin for Mutation Testing
3
+ * Creates targeted mutations for specific lines based on lineage tracking data
4
+ */
5
+
6
+ /**
7
+ * Creates a mutation testing plugin that targets a specific line and mutation type
8
+ * @param {number} targetLine - Line number to mutate
9
+ * @param {string} mutationType - Type of mutation to apply
10
+ * @param {Object} config - Mutation configuration
11
+ */
12
+ function createMutationPlugin(targetLine, mutationType, config = {}) {
13
+ return function({ types: t }, options = {}) {
14
+ return {
15
+ name: 'mutation-tester',
16
+ visitor: {
17
+ Program: {
18
+ enter(path, state) {
19
+ state.filename = state.file.opts.filename;
20
+ state.shouldMutate = false;
21
+ state.mutationApplied = false;
22
+ state.targetLine = targetLine;
23
+ state.mutationType = mutationType;
24
+ state.config = { ...config, ...options };
25
+ }
26
+ },
27
+
28
+ // ARITHMETIC OPERATORS: +, -, *, /, %
29
+ BinaryExpression(path, state) {
30
+ if (!shouldApplyMutation(path, state, 'arithmetic')) return;
31
+
32
+ const operator = path.node.operator;
33
+ const mutations = {
34
+ '+': '-', // Addition to Subtraction
35
+ '-': '+', // Subtraction to Addition
36
+ '*': '/', // Multiplication to Division
37
+ '/': '*', // Division to Multiplication
38
+ '%': '*', // Modulo to Multiplication
39
+ '==': '!=', // Equality to Inequality
40
+ '!=': '==', // Inequality to Equality
41
+ '===': '!==', // Strict equality to strict inequality
42
+ '!==': '===', // Strict inequality to strict equality
43
+ '<': '>=', // Less than to Greater/Equal
44
+ '>': '<=', // Greater than to Less/Equal
45
+ '<=': '>', // Less/Equal to Greater
46
+ '>=': '<' // Greater/Equal to Less
47
+ };
48
+
49
+ if (mutations[operator]) {
50
+ path.node.operator = mutations[operator];
51
+ state.mutationApplied = true;
52
+ logMutation(state, `${operator} → ${mutations[operator]}`);
53
+ }
54
+ },
55
+
56
+ // LOGICAL OPERATORS: &&, ||
57
+ LogicalExpression(path, state) {
58
+ if (!shouldApplyMutation(path, state, 'logical')) return;
59
+
60
+ const operator = path.node.operator;
61
+ const mutations = {
62
+ '&&': '||', // AND to OR
63
+ '||': '&&' // OR to AND
64
+ };
65
+
66
+ if (mutations[operator]) {
67
+ path.node.operator = mutations[operator];
68
+ state.mutationApplied = true;
69
+ logMutation(state, `${operator} → ${mutations[operator]}`);
70
+ }
71
+ },
72
+
73
+ // UNARY OPERATORS: !, ++, --
74
+ UnaryExpression(path, state) {
75
+ if (!shouldApplyMutation(path, state, 'logical')) return;
76
+
77
+ if (path.node.operator === '!') {
78
+ // Remove negation: !condition → condition
79
+ path.replaceWith(path.node.argument);
80
+ state.mutationApplied = true;
81
+ logMutation(state, '! → (removed)');
82
+ }
83
+ },
84
+
85
+ // UPDATE EXPRESSIONS: ++, --
86
+ UpdateExpression(path, state) {
87
+ if (!shouldApplyMutation(path, state, 'increments')) return;
88
+
89
+ const operator = path.node.operator;
90
+ const mutations = {
91
+ '++': '--', // Increment to Decrement
92
+ '--': '++' // Decrement to Increment
93
+ };
94
+
95
+ if (mutations[operator]) {
96
+ path.node.operator = mutations[operator];
97
+ state.mutationApplied = true;
98
+ logMutation(state, `${operator} → ${mutations[operator]}`);
99
+ }
100
+ },
101
+
102
+ // ASSIGNMENT OPERATORS: =, +=, -=, etc.
103
+ AssignmentExpression(path, state) {
104
+ if (!shouldApplyMutation(path, state, 'assignment')) return;
105
+
106
+ const operator = path.node.operator;
107
+ const mutations = {
108
+ '+=': '-=', // Add-assign to Subtract-assign
109
+ '-=': '+=', // Subtract-assign to Add-assign
110
+ '*=': '/=', // Multiply-assign to Divide-assign
111
+ '/=': '*=', // Divide-assign to Multiply-assign
112
+ '%=': '*=' // Modulo-assign to Multiply-assign
113
+ };
114
+
115
+ if (mutations[operator]) {
116
+ path.node.operator = mutations[operator];
117
+ state.mutationApplied = true;
118
+ logMutation(state, `${operator} → ${mutations[operator]}`);
119
+ }
120
+ },
121
+
122
+ // CONDITIONAL STATEMENTS: if, while, for
123
+ IfStatement(path, state) {
124
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
125
+
126
+ // Negate condition: if (x > 0) → if (!(x > 0))
127
+ const { types: t } = require('@babel/core');
128
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
129
+ state.mutationApplied = true;
130
+ logMutation(state, 'if condition → !(condition)');
131
+ },
132
+
133
+ // WHILE LOOPS
134
+ WhileStatement(path, state) {
135
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
136
+
137
+ const { types: t } = require('@babel/core');
138
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
139
+ state.mutationApplied = true;
140
+ logMutation(state, 'while condition → !(condition)');
141
+ },
142
+
143
+ // FOR LOOPS
144
+ ForStatement(path, state) {
145
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
146
+
147
+ // Only mutate if there's a test condition
148
+ if (path.node.test) {
149
+ const { types: t } = require('@babel/core');
150
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
151
+ state.mutationApplied = true;
152
+ logMutation(state, 'for condition → !(condition)');
153
+ }
154
+ },
155
+
156
+ // RETURN STATEMENTS
157
+ ReturnStatement(path, state) {
158
+ if (!shouldApplyMutation(path, state, 'returns')) return;
159
+
160
+ const { types: t } = require('@babel/core');
161
+
162
+ if (path.node.argument) {
163
+ // Apply type-safe mutations based on the argument type
164
+ if (t.isNumericLiteral(path.node.argument)) {
165
+ // For numbers, change to 0 instead of null to avoid type errors
166
+ const originalValue = path.node.argument.value;
167
+ const newValue = originalValue === 0 ? 1 : 0;
168
+ path.node.argument = t.numericLiteral(newValue);
169
+ state.mutationApplied = true;
170
+ logMutation(state, `return ${originalValue} → return ${newValue}`);
171
+ } else if (t.isBooleanLiteral(path.node.argument)) {
172
+ // For booleans, flip the value
173
+ const originalValue = path.node.argument.value;
174
+ path.node.argument = t.booleanLiteral(!originalValue);
175
+ state.mutationApplied = true;
176
+ logMutation(state, `return ${originalValue} → return ${!originalValue}`);
177
+ } else if (t.isStringLiteral(path.node.argument)) {
178
+ // For strings, change to empty string
179
+ const originalValue = path.node.argument.value;
180
+ path.node.argument = t.stringLiteral("");
181
+ state.mutationApplied = true;
182
+ logMutation(state, `return "${originalValue}" → return ""`);
183
+ } else if (t.isBinaryExpression(path.node.argument)) {
184
+ // For binary expressions like a + b, try to mutate the operator or operands
185
+ const expr = path.node.argument;
186
+ if (expr.operator === '+') {
187
+ // Change + to - for arithmetic expressions
188
+ expr.operator = '-';
189
+ state.mutationApplied = true;
190
+ logMutation(state, `return a + b → return a - b`);
191
+ } else if (expr.operator === '-') {
192
+ // Change - to + for arithmetic expressions
193
+ expr.operator = '+';
194
+ state.mutationApplied = true;
195
+ logMutation(state, `return a - b → return a + b`);
196
+ } else if (expr.operator === '*') {
197
+ // Change * to / for arithmetic expressions
198
+ expr.operator = '/';
199
+ state.mutationApplied = true;
200
+ logMutation(state, `return a * b → return a / b`);
201
+ } else if (expr.operator === '/') {
202
+ // Change / to * for arithmetic expressions
203
+ expr.operator = '*';
204
+ state.mutationApplied = true;
205
+ logMutation(state, `return a / b → return a * b`);
206
+ } else {
207
+ // For other binary expressions, skip to avoid breaking module loading
208
+ logMutation(state, `return expression → skipped (complex binary expression)`);
209
+ }
210
+ } else {
211
+ // For other complex expressions, skip mutation to avoid breaking module loading
212
+ // This is more conservative but safer
213
+ logMutation(state, `return expression → skipped (complex expression)`);
214
+ }
215
+ }
216
+ },
217
+
218
+ // LITERAL VALUES: numbers, booleans, strings
219
+ Literal(path, state) {
220
+ if (!shouldApplyMutation(path, state, 'literals')) return;
221
+
222
+ const value = path.node.value;
223
+
224
+ if (typeof value === 'number') {
225
+ // Mutate numbers: 5 → 0, 0 → 1, negative → positive
226
+ const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
227
+ path.node.value = newValue;
228
+ state.mutationApplied = true;
229
+ logMutation(state, `${value} → ${newValue}`);
230
+ } else if (typeof value === 'boolean') {
231
+ // Flip booleans: true → false, false → true
232
+ path.node.value = !value;
233
+ state.mutationApplied = true;
234
+ logMutation(state, `${value} → ${!value}`);
235
+ } else if (typeof value === 'string' && value.length > 0) {
236
+ // Empty strings: "hello" → ""
237
+ path.node.value = "";
238
+ state.mutationApplied = true;
239
+ logMutation(state, `"${value}" → ""`);
240
+ }
241
+ },
242
+
243
+ // NUMERIC LITERALS (for newer Babel versions)
244
+ NumericLiteral(path, state) {
245
+ if (!shouldApplyMutation(path, state, 'literals')) return;
246
+
247
+ const value = path.node.value;
248
+ const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
249
+ path.node.value = newValue;
250
+ state.mutationApplied = true;
251
+ logMutation(state, `${value} → ${newValue}`);
252
+ },
253
+
254
+ // BOOLEAN LITERALS
255
+ BooleanLiteral(path, state) {
256
+ if (!shouldApplyMutation(path, state, 'literals')) return;
257
+
258
+ const value = path.node.value;
259
+ path.node.value = !value;
260
+ state.mutationApplied = true;
261
+ logMutation(state, `${value} → ${!value}`);
262
+ },
263
+
264
+ // STRING LITERALS
265
+ StringLiteral(path, state) {
266
+ if (!shouldApplyMutation(path, state, 'literals')) return;
267
+
268
+ const value = path.node.value;
269
+ if (value.length > 0) {
270
+ path.node.value = "";
271
+ state.mutationApplied = true;
272
+ logMutation(state, `"${value}" → ""`);
273
+ }
274
+ }
275
+ }
276
+ };
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Determines if a mutation should be applied to the current node
282
+ */
283
+ function shouldApplyMutation(path, state, operatorType) {
284
+ const lineNumber = path.node.loc?.start.line;
285
+
286
+ // Check if this is the target line
287
+ if (lineNumber !== state.targetLine) {
288
+ return false;
289
+ }
290
+
291
+ // Check if this operator type is enabled
292
+ if (state.config.mutationOperators && !state.config.mutationOperators[operatorType]) {
293
+ return false;
294
+ }
295
+
296
+ // Check if we've already applied a mutation (one per line)
297
+ if (state.mutationApplied) {
298
+ return false;
299
+ }
300
+
301
+ return true;
302
+ }
303
+
304
+ /**
305
+ * Logs mutation information
306
+ */
307
+ function logMutation(state, description) {
308
+ if (state.config.enableDebugLogging) {
309
+ console.log(`🧬 Mutation applied at ${state.filename}:${state.targetLine} - ${description}`);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Gets all possible mutations for a given line of code by analyzing the AST
315
+ */
316
+ function getPossibleMutations(filePath, lineNumber, sourceCode) {
317
+ try {
318
+ const babel = require('@babel/core');
319
+ const fs = require('fs');
320
+
321
+ // Read the full file content
322
+ const fullFileContent = fs.readFileSync(filePath, 'utf8');
323
+ const lines = fullFileContent.split('\n');
324
+ const targetLine = lines[lineNumber - 1];
325
+
326
+ if (!targetLine) return [];
327
+
328
+ const possibleMutations = [];
329
+
330
+ // Parse the entire file to get proper AST context
331
+ const ast = babel.parseSync(fullFileContent, {
332
+ filename: filePath,
333
+ parserOpts: {
334
+ sourceType: "module",
335
+ allowImportExportEverywhere: true,
336
+ plugins: ["typescript", "jsx"],
337
+ },
338
+ });
339
+
340
+ // Traverse the AST to find nodes on the target line
341
+ babel.traverse(ast, {
342
+ enter(path) {
343
+ const nodeLineNumber = path.node.loc?.start.line;
344
+ if (nodeLineNumber !== lineNumber) return;
345
+
346
+ // Check what types of mutations are possible based on the AST nodes
347
+ if (path.isBinaryExpression()) {
348
+ const operator = path.node.operator;
349
+ if (['+', '-', '*', '/', '%'].includes(operator)) {
350
+ possibleMutations.push('arithmetic');
351
+ }
352
+ if (['==', '!=', '===', '!==', '<', '>', '<=', '>='].includes(operator)) {
353
+ possibleMutations.push('comparison');
354
+ }
355
+ }
356
+
357
+ if (path.isLogicalExpression()) {
358
+ possibleMutations.push('logical');
359
+ }
360
+
361
+ if (path.isUnaryExpression() && path.node.operator === '!') {
362
+ possibleMutations.push('logical');
363
+ }
364
+
365
+ if (path.isUpdateExpression()) {
366
+ possibleMutations.push('increments');
367
+ }
368
+
369
+ if (path.isAssignmentExpression() && path.node.operator !== '=') {
370
+ possibleMutations.push('assignment');
371
+ }
372
+
373
+ // Only add conditional mutations for actual conditional statements
374
+ if (path.isIfStatement() || path.isWhileStatement() ||
375
+ (path.isForStatement() && path.node.test)) {
376
+ possibleMutations.push('conditional');
377
+ }
378
+
379
+ if (path.isReturnStatement()) {
380
+ possibleMutations.push('returns');
381
+ }
382
+
383
+ if (path.isLiteral() || path.isNumericLiteral() ||
384
+ path.isBooleanLiteral() || path.isStringLiteral()) {
385
+ possibleMutations.push('literals');
386
+ }
387
+ }
388
+ });
389
+
390
+ // Remove duplicates
391
+ return [...new Set(possibleMutations)];
392
+
393
+ } catch (error) {
394
+ // If AST analysis fails, return empty array to skip this line
395
+ return [];
396
+ }
397
+ }
398
+
399
+ module.exports = {
400
+ createMutationPlugin,
401
+ getPossibleMutations
402
+ };