@mutineerjs/mutineer 0.2.4 → 0.4.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/README.md +48 -42
- package/dist/bin/mutineer.js +0 -0
- package/dist/mutators/__tests__/operator.spec.js +169 -0
- package/dist/mutators/__tests__/registry.spec.js +6 -0
- package/dist/mutators/__tests__/return-value.spec.js +239 -0
- package/dist/mutators/__tests__/utils.spec.js +68 -1
- package/dist/mutators/operator.d.ts +25 -0
- package/dist/mutators/operator.js +50 -0
- package/dist/mutators/registry.d.ts +6 -28
- package/dist/mutators/registry.js +14 -66
- package/dist/mutators/return-value.d.ts +39 -0
- package/dist/mutators/return-value.js +104 -0
- package/dist/mutators/utils.d.ts +21 -0
- package/dist/mutators/utils.js +44 -27
- package/dist/runner/__tests__/pool-executor.spec.js +95 -4
- package/dist/runner/__tests__/variants.spec.js +3 -1
- package/dist/runner/discover.js +2 -1
- package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
- package/dist/runner/jest/__tests__/pool.spec.js +5 -6
- package/dist/runner/jest/__tests__/worker-runtime.spec.js +2 -2
- package/dist/runner/jest/adapter.js +1 -1
- package/dist/runner/jest/pool.d.ts +1 -1
- package/dist/runner/jest/pool.js +6 -6
- package/dist/runner/jest/worker.mjs +1 -1
- package/dist/runner/pool-executor.js +21 -1
- package/dist/runner/shared/__tests__/redirect-state.spec.js +3 -3
- package/dist/runner/shared/index.d.ts +1 -1
- package/dist/runner/shared/index.js +1 -1
- package/dist/runner/shared/redirect-state.d.ts +2 -2
- package/dist/runner/shared/redirect-state.js +4 -4
- package/dist/runner/types.d.ts +1 -1
- package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +83 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
- package/dist/runner/vitest/adapter.js +1 -1
- package/dist/runner/vitest/index.d.ts +0 -1
- package/dist/runner/vitest/index.js +0 -1
- package/dist/runner/vitest/pool.d.ts +1 -1
- package/dist/runner/vitest/pool.js +7 -7
- package/dist/runner/vitest/redirect-loader.d.ts +1 -1
- package/dist/runner/vitest/redirect-loader.js +1 -1
- package/dist/runner/vitest/worker-runtime.js +3 -3
- package/dist/runner/vitest/worker.mjs +1 -1
- package/dist/types/mutant.d.ts +2 -0
- package/dist/utils/__tests__/coverage.spec.js +167 -0
- package/dist/utils/__tests__/progress.spec.js +96 -0
- package/dist/utils/__tests__/summary.spec.js +28 -0
- package/dist/utils/summary.js +7 -1
- package/package.json +71 -22
- package/dist/admin/assets/index-B7nXq-e7.js +0 -32
- package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
- package/dist/admin/assets/index-BDQLkBUE.js +0 -32
- package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
- package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
- package/dist/admin/index.html +0 -13
- package/dist/admin/server/admin.d.ts +0 -6
- package/dist/admin/server/admin.js +0 -234
- package/dist/bin/mutate-vitest.d.ts +0 -2
- package/dist/bin/mutate-vitest.js +0 -90
- package/dist/plugin/viteMutate.d.ts +0 -15
- package/dist/plugin/viteMutate.js +0 -52
- package/dist/plugin/vitest.setup.d.ts +0 -47
- package/dist/plugin/vitest.setup.js +0 -118
- package/dist/plugin/withVitest.d.ts +0 -13
- package/dist/plugin/withVitest.js +0 -30
- package/dist/runner/__tests__/orchestrator.spec.js +0 -55
- package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +0 -1
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
- package/dist/runner/adapters/__tests__/vitest.spec.d.ts +0 -1
- package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
- package/dist/runner/adapters/index.d.ts +0 -10
- package/dist/runner/adapters/index.js +0 -9
- package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +0 -1
- package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
- package/dist/runner/adapters/jest/index.d.ts +0 -24
- package/dist/runner/adapters/jest/index.js +0 -216
- package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
- package/dist/runner/adapters/jest/worker-runtime.js +0 -171
- package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
- package/dist/runner/adapters/jest-worker-runtime.js +0 -171
- package/dist/runner/adapters/jest.d.ts +0 -24
- package/dist/runner/adapters/jest.js +0 -216
- package/dist/runner/adapters/types.d.ts +0 -89
- package/dist/runner/adapters/types.js +0 -8
- package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +0 -1
- package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +0 -1
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
- package/dist/runner/adapters/vitest/index.d.ts +0 -33
- package/dist/runner/adapters/vitest/index.js +0 -267
- package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
- package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
- package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
- package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
- package/dist/runner/adapters/vitest.d.ts +0 -33
- package/dist/runner/adapters/vitest.js +0 -267
- package/dist/runner/pool/__tests__/index.spec.d.ts +0 -1
- package/dist/runner/pool/__tests__/index.spec.js +0 -83
- package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +0 -1
- package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +0 -1
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
- package/dist/runner/pool/index.d.ts +0 -8
- package/dist/runner/pool/index.js +0 -9
- package/dist/runner/pool/jest/pool.d.ts +0 -52
- package/dist/runner/pool/jest/pool.js +0 -309
- package/dist/runner/pool/jest/worker.d.mts +0 -1
- package/dist/runner/pool/jest/worker.mjs +0 -60
- package/dist/runner/pool/jest-pool.d.ts +0 -52
- package/dist/runner/pool/jest-pool.js +0 -309
- package/dist/runner/pool/jest-worker.d.mts +0 -1
- package/dist/runner/pool/jest-worker.mjs +0 -60
- package/dist/runner/pool/plugin.d.ts +0 -18
- package/dist/runner/pool/plugin.js +0 -60
- package/dist/runner/pool/pool-plugin.d.ts +0 -18
- package/dist/runner/pool/pool-plugin.js +0 -60
- package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
- package/dist/runner/pool/pool-redirect-loader.js +0 -116
- package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
- package/dist/runner/pool/redirect-loader.d.ts +0 -19
- package/dist/runner/pool/redirect-loader.js +0 -116
- package/dist/runner/pool/vitest/pool.d.ts +0 -70
- package/dist/runner/pool/vitest/pool.js +0 -376
- package/dist/runner/pool/vitest/worker.d.mts +0 -15
- package/dist/runner/pool/vitest/worker.mjs +0 -96
- package/dist/runner/pool/vitest-worker.d.mts +0 -15
- package/dist/runner/pool/vitest-worker.mjs +0 -96
- package/dist/runner/shared-module-redirect.d.ts +0 -56
- package/dist/runner/shared-module-redirect.js +0 -84
- package/dist/types/api.d.ts +0 -20
- package/dist/types/api.js +0 -1
- /package/dist/{runner/__tests__/orchestrator.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
- /package/dist/{runner/adapters/__tests__/jest.spec.d.ts → mutators/__tests__/return-value.spec.d.ts} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator mutators.
|
|
3
|
+
*
|
|
4
|
+
* Each mutator finds every occurrence of a specific binary/logical operator
|
|
5
|
+
* using AST traversal and produces a mutated source string with that operator
|
|
6
|
+
* replaced by its counterpart.
|
|
7
|
+
*/
|
|
8
|
+
import { collectOperatorTargets } from './utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Factory to build an operator mutator using AST traversal and token analysis.
|
|
11
|
+
*
|
|
12
|
+
* @param name - Mutator name used in the registry and config include/exclude
|
|
13
|
+
* @param description - Human-readable description shown in reports
|
|
14
|
+
* @param fromOp - The operator to find (e.g., '&&')
|
|
15
|
+
* @param toOp - The operator to replace it with (e.g., '||')
|
|
16
|
+
*/
|
|
17
|
+
function makeOperatorMutator(name, description, fromOp, toOp) {
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
description,
|
|
21
|
+
apply(src) {
|
|
22
|
+
return collectOperatorTargets(src, fromOp).map((target) => ({
|
|
23
|
+
line: target.line,
|
|
24
|
+
col: target.col1,
|
|
25
|
+
code: src.slice(0, target.start) + toOp + src.slice(target.end),
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/* === Boundary mutators === */
|
|
31
|
+
export const relaxLE = makeOperatorMutator('relaxLE', "Change '<=' to '<' (relax boundary)", '<=', '<');
|
|
32
|
+
export const relaxGE = makeOperatorMutator('relaxGE', "Change '>=' to '>' (relax boundary)", '>=', '>');
|
|
33
|
+
export const tightenLT = makeOperatorMutator('tightenLT', "Change '<' to '<=' (tighten boundary)", '<', '<=');
|
|
34
|
+
export const tightenGT = makeOperatorMutator('tightenGT', "Change '>' to '>=' (tighten boundary)", '>', '>=');
|
|
35
|
+
/* === Logical mutators === */
|
|
36
|
+
export const andToOr = makeOperatorMutator('andToOr', "Change '&&' to '||'", '&&', '||');
|
|
37
|
+
export const orToAnd = makeOperatorMutator('orToAnd', "Change '||' to '&&'", '||', '&&');
|
|
38
|
+
export const nullishToOr = makeOperatorMutator('nullishToOr', "Change '??' to '||'", '??', '||');
|
|
39
|
+
/* === Equality mutators === */
|
|
40
|
+
export const flipEQ = makeOperatorMutator('flipEQ', "Change '==' to '!='", '==', '!=');
|
|
41
|
+
export const flipNEQ = makeOperatorMutator('flipNEQ', "Change '!=' to '=='", '!=', '==');
|
|
42
|
+
export const flipStrictEQ = makeOperatorMutator('flipStrictEQ', "Change '===' to '!=='", '===', '!==');
|
|
43
|
+
export const flipStrictNEQ = makeOperatorMutator('flipStrictNEQ', "Change '!==' to '==='", '!==', '===');
|
|
44
|
+
/* === Arithmetic mutators === */
|
|
45
|
+
export const addToSub = makeOperatorMutator('addToSub', "Change '+' to '-'", '+', '-');
|
|
46
|
+
export const subToAdd = makeOperatorMutator('subToAdd', "Change '-' to '+'", '-', '+');
|
|
47
|
+
export const mulToDiv = makeOperatorMutator('mulToDiv', "Change '*' to '/'", '*', '/');
|
|
48
|
+
export const divToMul = makeOperatorMutator('divToMul', "Change '/' to '*'", '/', '*');
|
|
49
|
+
export const modToMul = makeOperatorMutator('modToMul', "Change '%' to '*'", '%', '*');
|
|
50
|
+
export const powerToMul = makeOperatorMutator('powerToMul', "Change '**' to '*'", '**', '*');
|
|
@@ -1,37 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mutator registry
|
|
2
|
+
* Mutator registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* with optional include/exclude filtering.
|
|
4
|
+
* Aggregates all mutators and exposes `getRegistry` for filtered access.
|
|
6
5
|
*/
|
|
7
6
|
import type { ASTMutator } from './types.js';
|
|
8
|
-
export declare const relaxLE: ASTMutator;
|
|
9
|
-
export declare const relaxGE: ASTMutator;
|
|
10
|
-
export declare const tightenLT: ASTMutator;
|
|
11
|
-
export declare const tightenGT: ASTMutator;
|
|
12
|
-
export declare const andToOr: ASTMutator;
|
|
13
|
-
export declare const orToAnd: ASTMutator;
|
|
14
|
-
export declare const nullishToOr: ASTMutator;
|
|
15
|
-
export declare const flipEQ: ASTMutator;
|
|
16
|
-
export declare const flipNEQ: ASTMutator;
|
|
17
|
-
export declare const flipStrictEQ: ASTMutator;
|
|
18
|
-
export declare const flipStrictNEQ: ASTMutator;
|
|
19
|
-
export declare const addToSub: ASTMutator;
|
|
20
|
-
export declare const subToAdd: ASTMutator;
|
|
21
|
-
export declare const mulToDiv: ASTMutator;
|
|
22
|
-
export declare const divToMul: ASTMutator;
|
|
23
|
-
export declare const modToMul: ASTMutator;
|
|
24
|
-
export declare const powerToMul: ASTMutator;
|
|
25
7
|
/**
|
|
26
|
-
* Get a filtered
|
|
8
|
+
* Get a filtered list of mutators.
|
|
27
9
|
*
|
|
28
|
-
* If include
|
|
29
|
-
* If exclude
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @param include - Optional list of mutator names to include
|
|
33
|
-
* @param exclude - Optional list of mutator names to exclude
|
|
34
|
-
* @returns Filtered array of mutators
|
|
10
|
+
* If `include` is provided, only those mutators are returned.
|
|
11
|
+
* If `exclude` is provided, those mutators are removed.
|
|
12
|
+
* `include` takes precedence over `exclude`.
|
|
35
13
|
*/
|
|
36
14
|
export declare function getRegistry(include?: readonly string[], exclude?: readonly string[]): ASTMutator[];
|
|
37
15
|
export type { ASTMutator, AnyMutator, MutationOutput } from './types.js';
|
|
@@ -1,62 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mutator registry
|
|
2
|
+
* Mutator registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* with optional include/exclude filtering.
|
|
6
|
-
*/
|
|
7
|
-
import { collectOperatorTargets } from './utils.js';
|
|
8
|
-
/**
|
|
9
|
-
* Factory to build an operator mutator using AST traversal and token analysis.
|
|
10
|
-
* Creates a reusable mutator that finds and replaces a specific operator throughout the code.
|
|
11
|
-
*
|
|
12
|
-
* @param name - Name of the mutator (e.g., 'andToOr')
|
|
13
|
-
* @param description - Human-readable description
|
|
14
|
-
* @param fromOp - The operator to find (e.g., '&&')
|
|
15
|
-
* @param toOp - The operator to replace it with (e.g., '||')
|
|
16
|
-
* @returns An ASTMutator that applies this transformation
|
|
17
|
-
*/
|
|
18
|
-
function makeOperatorMutator(name, description, fromOp, toOp) {
|
|
19
|
-
return {
|
|
20
|
-
name,
|
|
21
|
-
description,
|
|
22
|
-
apply(src) {
|
|
23
|
-
// 1) Collect exact operator token locations from matching nodes
|
|
24
|
-
const targets = collectOperatorTargets(src, fromOp);
|
|
25
|
-
// 2) For each occurrence, produce a mutated version of the programme
|
|
26
|
-
const outputs = [];
|
|
27
|
-
for (const target of targets) {
|
|
28
|
-
const code = src.slice(0, target.start) + toOp + src.slice(target.end);
|
|
29
|
-
outputs.push({
|
|
30
|
-
line: target.line,
|
|
31
|
-
col: target.col1,
|
|
32
|
-
code,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return outputs;
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/* === Concrete mutators (AST-safe) === */
|
|
40
|
-
export const relaxLE = makeOperatorMutator('relaxLE', "Change '<=' to '<' (relax boundary)", '<=', '<');
|
|
41
|
-
export const relaxGE = makeOperatorMutator('relaxGE', "Change '>=' to '>' (relax boundary)", '>=', '>');
|
|
42
|
-
export const tightenLT = makeOperatorMutator('tightenLT', "Change '<' to '<=' (tighten boundary)", '<', '<=');
|
|
43
|
-
export const tightenGT = makeOperatorMutator('tightenGT', "Change '>' to '>=' (tighten boundary)", '>', '>=');
|
|
44
|
-
export const andToOr = makeOperatorMutator('andToOr', "Change '&&' to '||' in boolean expressions", '&&', '||');
|
|
45
|
-
export const orToAnd = makeOperatorMutator('orToAnd', "Change '||' to '&&' in boolean expressions", '||', '&&');
|
|
46
|
-
export const nullishToOr = makeOperatorMutator('nullishToOr', "Change '??' to '||' to prefer boolean fallback", '??', '||');
|
|
47
|
-
export const flipEQ = makeOperatorMutator('flipEQ', "Change '==' to '!='", '==', '!=');
|
|
48
|
-
export const flipNEQ = makeOperatorMutator('flipNEQ', "Change '!=' to '=='", '!=', '==');
|
|
49
|
-
export const flipStrictEQ = makeOperatorMutator('flipStrictEQ', "Change '===' to '!=='", '===', '!==');
|
|
50
|
-
export const flipStrictNEQ = makeOperatorMutator('flipStrictNEQ', "Change '!==' to '==='", '!==', '===');
|
|
51
|
-
export const addToSub = makeOperatorMutator('addToSub', "Change '+' to '-' in arithmetic expressions", '+', '-');
|
|
52
|
-
export const subToAdd = makeOperatorMutator('subToAdd', "Change '-' to '+' in arithmetic expressions", '-', '+');
|
|
53
|
-
export const mulToDiv = makeOperatorMutator('mulToDiv', "Change '*' to '/' in arithmetic expressions", '*', '/');
|
|
54
|
-
export const divToMul = makeOperatorMutator('divToMul', "Change '/' to '*' in arithmetic expressions", '/', '*');
|
|
55
|
-
export const modToMul = makeOperatorMutator('modToMul', "Change '%' to '*' in arithmetic expressions", '%', '*');
|
|
56
|
-
export const powerToMul = makeOperatorMutator('powerToMul', "Change '**' to '*' in arithmetic expressions", '**', '*');
|
|
57
|
-
/**
|
|
58
|
-
* All registered mutators in order of precedence.
|
|
4
|
+
* Aggregates all mutators and exposes `getRegistry` for filtered access.
|
|
59
5
|
*/
|
|
6
|
+
import { relaxLE, relaxGE, tightenLT, tightenGT, andToOr, orToAnd, nullishToOr, flipEQ, flipNEQ, flipStrictEQ, flipStrictNEQ, addToSub, subToAdd, mulToDiv, divToMul, modToMul, powerToMul, } from './operator.js';
|
|
7
|
+
import { returnToNull, returnToUndefined, returnFlipBool, returnZero, returnEmptyStr, returnEmptyArr, } from './return-value.js';
|
|
60
8
|
const ALL = [
|
|
61
9
|
relaxLE,
|
|
62
10
|
relaxGE,
|
|
@@ -75,25 +23,25 @@ const ALL = [
|
|
|
75
23
|
divToMul,
|
|
76
24
|
modToMul,
|
|
77
25
|
powerToMul,
|
|
26
|
+
returnToNull,
|
|
27
|
+
returnToUndefined,
|
|
28
|
+
returnFlipBool,
|
|
29
|
+
returnZero,
|
|
30
|
+
returnEmptyStr,
|
|
31
|
+
returnEmptyArr,
|
|
78
32
|
];
|
|
79
33
|
/**
|
|
80
|
-
* Get a filtered
|
|
81
|
-
*
|
|
82
|
-
* If include list provided, only those mutators are returned.
|
|
83
|
-
* If exclude list provided, those mutators are filtered out.
|
|
84
|
-
* Include list takes precedence over exclude list.
|
|
34
|
+
* Get a filtered list of mutators.
|
|
85
35
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
36
|
+
* If `include` is provided, only those mutators are returned.
|
|
37
|
+
* If `exclude` is provided, those mutators are removed.
|
|
38
|
+
* `include` takes precedence over `exclude`.
|
|
89
39
|
*/
|
|
90
40
|
export function getRegistry(include, exclude) {
|
|
91
41
|
let list = ALL;
|
|
92
|
-
// If include list provided, filter to only those mutators
|
|
93
42
|
if (include?.length) {
|
|
94
43
|
list = list.filter((m) => include.includes(m.name));
|
|
95
44
|
}
|
|
96
|
-
// If exclude list provided, remove those mutators
|
|
97
45
|
if (exclude?.length) {
|
|
98
46
|
list = list.filter((m) => !exclude.includes(m.name));
|
|
99
47
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return value mutators.
|
|
3
|
+
*
|
|
4
|
+
* These mutators target `return` statements and replace the returned expression
|
|
5
|
+
* with a simpler or opposite value. They catch a different class of test gap
|
|
6
|
+
* from operator mutators: they reveal when callers never check what a function
|
|
7
|
+
* returns (e.g. no null-check, no assertion on the return value).
|
|
8
|
+
*/
|
|
9
|
+
import type { ASTMutator } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Replace any non-null return value with `null`.
|
|
12
|
+
* Reveals callers that never check for null returns.
|
|
13
|
+
*/
|
|
14
|
+
export declare const returnToNull: ASTMutator;
|
|
15
|
+
/**
|
|
16
|
+
* Replace any non-undefined return value with `undefined`.
|
|
17
|
+
* Reveals callers that never guard against undefined returns.
|
|
18
|
+
*/
|
|
19
|
+
export declare const returnToUndefined: ASTMutator;
|
|
20
|
+
/**
|
|
21
|
+
* Flip `return true` ↔ `return false`.
|
|
22
|
+
* Catches missing assertions on boolean-returning functions.
|
|
23
|
+
*/
|
|
24
|
+
export declare const returnFlipBool: ASTMutator;
|
|
25
|
+
/**
|
|
26
|
+
* Replace a non-zero numeric return with `0`.
|
|
27
|
+
* Catches callers that never check the numeric return value.
|
|
28
|
+
*/
|
|
29
|
+
export declare const returnZero: ASTMutator;
|
|
30
|
+
/**
|
|
31
|
+
* Replace a non-empty string return with `''`.
|
|
32
|
+
* Catches callers that never check the string return value.
|
|
33
|
+
*/
|
|
34
|
+
export declare const returnEmptyStr: ASTMutator;
|
|
35
|
+
/**
|
|
36
|
+
* Replace an array expression return with `[]`.
|
|
37
|
+
* Catches callers that never check for an empty array.
|
|
38
|
+
*/
|
|
39
|
+
export declare const returnEmptyArr: ASTMutator;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return value mutators.
|
|
3
|
+
*
|
|
4
|
+
* These mutators target `return` statements and replace the returned expression
|
|
5
|
+
* with a simpler or opposite value. They catch a different class of test gap
|
|
6
|
+
* from operator mutators: they reveal when callers never check what a function
|
|
7
|
+
* returns (e.g. no null-check, no assertion on the return value).
|
|
8
|
+
*/
|
|
9
|
+
import * as t from '@babel/types';
|
|
10
|
+
import { traverse, getVisualColumn, parseSource, buildIgnoreLines, } from './utils.js';
|
|
11
|
+
/**
|
|
12
|
+
* Factory for return-value mutators.
|
|
13
|
+
*
|
|
14
|
+
* Traverses all `ReturnStatement` nodes in the source. For each node that has
|
|
15
|
+
* an argument (i.e. not a bare `return;`), calls `replacer` with the argument
|
|
16
|
+
* AST node. If `replacer` returns a non-null string, emits a mutation that
|
|
17
|
+
* splices that string in place of the original argument.
|
|
18
|
+
*
|
|
19
|
+
* @param name - Mutator name used in the registry and config include/exclude
|
|
20
|
+
* @param description - Human-readable description shown in reports
|
|
21
|
+
* @param replacer - Returns the replacement source text, or null to skip
|
|
22
|
+
*/
|
|
23
|
+
function makeReturnMutator(name, description, replacer) {
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
description,
|
|
27
|
+
apply(src) {
|
|
28
|
+
const ast = parseSource(src);
|
|
29
|
+
const fileAst = ast;
|
|
30
|
+
const ignoreLines = buildIgnoreLines(fileAst.comments ?? []);
|
|
31
|
+
const outputs = [];
|
|
32
|
+
traverse(ast, {
|
|
33
|
+
ReturnStatement(path) {
|
|
34
|
+
const node = path.node;
|
|
35
|
+
if (!node.argument)
|
|
36
|
+
return; // bare return; — nothing to replace
|
|
37
|
+
const line = node.loc?.start.line;
|
|
38
|
+
if (line === undefined)
|
|
39
|
+
return;
|
|
40
|
+
if (ignoreLines.has(line))
|
|
41
|
+
return;
|
|
42
|
+
const replacement = replacer(node.argument);
|
|
43
|
+
if (replacement === null)
|
|
44
|
+
return;
|
|
45
|
+
const argStart = node.argument.start;
|
|
46
|
+
const argEnd = node.argument.end;
|
|
47
|
+
if (argStart == null || argEnd == null)
|
|
48
|
+
return;
|
|
49
|
+
const col = getVisualColumn(src, node.start ?? 0);
|
|
50
|
+
const code = src.slice(0, argStart) + replacement + src.slice(argEnd);
|
|
51
|
+
outputs.push({ line, col, code });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return outputs;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/* === Concrete return-value mutators === */
|
|
59
|
+
/**
|
|
60
|
+
* Replace any non-null return value with `null`.
|
|
61
|
+
* Reveals callers that never check for null returns.
|
|
62
|
+
*/
|
|
63
|
+
export const returnToNull = makeReturnMutator('returnToNull', 'Replace return value with null', (node) => (t.isNullLiteral(node) ? null : 'null'));
|
|
64
|
+
/**
|
|
65
|
+
* Replace any non-undefined return value with `undefined`.
|
|
66
|
+
* Reveals callers that never guard against undefined returns.
|
|
67
|
+
*/
|
|
68
|
+
export const returnToUndefined = makeReturnMutator('returnToUndefined', 'Replace return value with undefined', (node) => (t.isIdentifier(node, { name: 'undefined' }) ? null : 'undefined'));
|
|
69
|
+
/**
|
|
70
|
+
* Flip `return true` ↔ `return false`.
|
|
71
|
+
* Catches missing assertions on boolean-returning functions.
|
|
72
|
+
*/
|
|
73
|
+
export const returnFlipBool = makeReturnMutator('returnFlipBool', 'Flip boolean return value (true ↔ false)', (node) => {
|
|
74
|
+
if (!t.isBooleanLiteral(node))
|
|
75
|
+
return null;
|
|
76
|
+
return node.value ? 'false' : 'true';
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Replace a non-zero numeric return with `0`.
|
|
80
|
+
* Catches callers that never check the numeric return value.
|
|
81
|
+
*/
|
|
82
|
+
export const returnZero = makeReturnMutator('returnZero', 'Replace numeric return value with 0', (node) => {
|
|
83
|
+
if (!t.isNumericLiteral(node))
|
|
84
|
+
return null;
|
|
85
|
+
return node.value === 0 ? null : '0';
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Replace a non-empty string return with `''`.
|
|
89
|
+
* Catches callers that never check the string return value.
|
|
90
|
+
*/
|
|
91
|
+
export const returnEmptyStr = makeReturnMutator('returnEmptyStr', "Replace string return value with ''", (node) => {
|
|
92
|
+
if (!t.isStringLiteral(node))
|
|
93
|
+
return null;
|
|
94
|
+
return node.value === '' ? null : "''";
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Replace an array expression return with `[]`.
|
|
98
|
+
* Catches callers that never check for an empty array.
|
|
99
|
+
*/
|
|
100
|
+
export const returnEmptyArr = makeReturnMutator('returnEmptyArr', 'Replace array return value with []', (node) => {
|
|
101
|
+
if (!t.isArrayExpression(node))
|
|
102
|
+
return null;
|
|
103
|
+
return node.elements.length === 0 ? null : '[]';
|
|
104
|
+
});
|
package/dist/mutators/utils.d.ts
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
* Provides functions for parsing, traversing, and analyzing code ASTs.
|
|
5
5
|
* These utilities are used by operator mutators to locate and replace operators.
|
|
6
6
|
*/
|
|
7
|
+
import { type ParserOptions } from '@babel/parser';
|
|
7
8
|
import * as t from '@babel/types';
|
|
8
9
|
import type { OperatorTarget } from './types.js';
|
|
9
10
|
export declare const traverse: typeof import('@babel/traverse').default;
|
|
11
|
+
/**
|
|
12
|
+
* Parser configuration for Babel.
|
|
13
|
+
* Enables support for TypeScript, JSX, decorators, and modern JavaScript features.
|
|
14
|
+
*/
|
|
15
|
+
export declare const parserOptsTs: ParserOptions;
|
|
16
|
+
export declare const parserOptsFlow: ParserOptions;
|
|
10
17
|
/**
|
|
11
18
|
* Tab width used for converting character columns to visual columns.
|
|
12
19
|
* This helps report correct column positions for terminals that render tabs.
|
|
@@ -21,6 +28,20 @@ export declare const TAB_WIDTH = 4;
|
|
|
21
28
|
* @returns The visual column (1-based)
|
|
22
29
|
*/
|
|
23
30
|
export declare function getVisualColumn(src: string, charOffset: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Build a set of line numbers that should be ignored for mutation based on
|
|
33
|
+
* disable comments (`mutineer-disable`, `mutineer-disable-line`,
|
|
34
|
+
* `mutineer-disable-next-line`).
|
|
35
|
+
*
|
|
36
|
+
* @param comments - The comment nodes from the parsed AST
|
|
37
|
+
* @returns Set of 1-based line numbers that should not be mutated
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildIgnoreLines(comments: readonly t.Comment[]): Set<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Parse source with the TypeScript Babel plugin, falling back to the Flow
|
|
42
|
+
* plugin for Flow-typed React files that fail TS parsing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSource(src: string): import("@babel/parser").ParseResult<t.File>;
|
|
24
45
|
/**
|
|
25
46
|
* Type guard to check if a node is a BinaryExpression or LogicalExpression.
|
|
26
47
|
*/
|
package/dist/mutators/utils.js
CHANGED
|
@@ -13,7 +13,7 @@ export const traverse = traverseModuleNormalized.default ?? traverseModuleNormal
|
|
|
13
13
|
* Parser configuration for Babel.
|
|
14
14
|
* Enables support for TypeScript, JSX, decorators, and modern JavaScript features.
|
|
15
15
|
*/
|
|
16
|
-
const parserOptsTs = {
|
|
16
|
+
export const parserOptsTs = {
|
|
17
17
|
sourceType: 'unambiguous',
|
|
18
18
|
plugins: [
|
|
19
19
|
'typescript',
|
|
@@ -28,7 +28,7 @@ const parserOptsTs = {
|
|
|
28
28
|
],
|
|
29
29
|
tokens: true,
|
|
30
30
|
};
|
|
31
|
-
const parserOptsFlow = {
|
|
31
|
+
export const parserOptsFlow = {
|
|
32
32
|
sourceType: 'unambiguous',
|
|
33
33
|
plugins: [
|
|
34
34
|
'flow',
|
|
@@ -74,33 +74,14 @@ export function getVisualColumn(src, charOffset) {
|
|
|
74
74
|
return visualCol;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return node.type === 'BinaryExpression' || node.type === 'LogicalExpression';
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Collect the operator tokens for a given operator and return their exact locations.
|
|
84
|
-
* Uses AST traversal to find BinaryExpression/LogicalExpression nodes, then maps them
|
|
85
|
-
* to token positions for accurate column reporting.
|
|
77
|
+
* Build a set of line numbers that should be ignored for mutation based on
|
|
78
|
+
* disable comments (`mutineer-disable`, `mutineer-disable-line`,
|
|
79
|
+
* `mutineer-disable-next-line`).
|
|
86
80
|
*
|
|
87
|
-
* @param
|
|
88
|
-
* @
|
|
89
|
-
* @returns Array of target locations for the operator
|
|
81
|
+
* @param comments - The comment nodes from the parsed AST
|
|
82
|
+
* @returns Set of 1-based line numbers that should not be mutated
|
|
90
83
|
*/
|
|
91
|
-
export function
|
|
92
|
-
let ast;
|
|
93
|
-
try {
|
|
94
|
-
ast = parse(src, parserOptsTs);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Flow-typed React sources fail under TS parsing; fall back to Flow plugins.
|
|
98
|
-
ast = parse(src, parserOptsFlow);
|
|
99
|
-
}
|
|
100
|
-
const fileAst = ast;
|
|
101
|
-
const tokens = fileAst.tokens ?? [];
|
|
102
|
-
const comments = fileAst.comments ?? [];
|
|
103
|
-
const out = [];
|
|
84
|
+
export function buildIgnoreLines(comments) {
|
|
104
85
|
const ignoreLines = new Set();
|
|
105
86
|
for (const comment of comments) {
|
|
106
87
|
const text = comment.value.trim();
|
|
@@ -120,6 +101,42 @@ export function collectOperatorTargets(src, opValue) {
|
|
|
120
101
|
}
|
|
121
102
|
}
|
|
122
103
|
}
|
|
104
|
+
return ignoreLines;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse source with the TypeScript Babel plugin, falling back to the Flow
|
|
108
|
+
* plugin for Flow-typed React files that fail TS parsing.
|
|
109
|
+
*/
|
|
110
|
+
export function parseSource(src) {
|
|
111
|
+
try {
|
|
112
|
+
return parse(src, parserOptsTs);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return parse(src, parserOptsFlow);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Type guard to check if a node is a BinaryExpression or LogicalExpression.
|
|
120
|
+
*/
|
|
121
|
+
export function isBinaryOrLogical(node) {
|
|
122
|
+
return node.type === 'BinaryExpression' || node.type === 'LogicalExpression';
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Collect the operator tokens for a given operator and return their exact locations.
|
|
126
|
+
* Uses AST traversal to find BinaryExpression/LogicalExpression nodes, then maps them
|
|
127
|
+
* to token positions for accurate column reporting.
|
|
128
|
+
*
|
|
129
|
+
* @param src - The source code
|
|
130
|
+
* @param opValue - The operator to search for (e.g., '&&', '<=')
|
|
131
|
+
* @returns Array of target locations for the operator
|
|
132
|
+
*/
|
|
133
|
+
export function collectOperatorTargets(src, opValue) {
|
|
134
|
+
const ast = parseSource(src);
|
|
135
|
+
const fileAst = ast;
|
|
136
|
+
const tokens = fileAst.tokens ?? [];
|
|
137
|
+
const comments = fileAst.comments ?? [];
|
|
138
|
+
const out = [];
|
|
139
|
+
const ignoreLines = buildIgnoreLines(comments);
|
|
123
140
|
traverse(ast, {
|
|
124
141
|
enter(p) {
|
|
125
142
|
if (!isBinaryOrLogical(p.node))
|
|
@@ -8,9 +8,7 @@ function makeAdapter(overrides = {}) {
|
|
|
8
8
|
name: 'vitest',
|
|
9
9
|
init: vi.fn().mockResolvedValue(undefined),
|
|
10
10
|
runBaseline: vi.fn().mockResolvedValue(true),
|
|
11
|
-
runMutant: vi
|
|
12
|
-
.fn()
|
|
13
|
-
.mockResolvedValue({ status: 'killed', durationMs: 10 }),
|
|
11
|
+
runMutant: vi.fn().mockResolvedValue({ status: 'killed', durationMs: 10 }),
|
|
14
12
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
15
13
|
hasCoverageProvider: vi.fn().mockReturnValue(false),
|
|
16
14
|
detectCoverageConfig: vi
|
|
@@ -45,7 +43,7 @@ describe('executePool', () => {
|
|
|
45
43
|
process.exitCode = undefined;
|
|
46
44
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
47
45
|
});
|
|
48
|
-
it('
|
|
46
|
+
it('initialises adapter with correct worker count', async () => {
|
|
49
47
|
const adapter = makeAdapter();
|
|
50
48
|
const cache = {};
|
|
51
49
|
const tasks = [makeTask()];
|
|
@@ -195,6 +193,99 @@ describe('executePool', () => {
|
|
|
195
193
|
const content = JSON.parse(await fs.readFile(cacheFile, 'utf8'));
|
|
196
194
|
expect(content['persist-key']).toBeDefined();
|
|
197
195
|
});
|
|
196
|
+
it('escaped mutant stores snippets when lines differ', async () => {
|
|
197
|
+
const tmpFile = path.join(tmpDir, 'source.ts');
|
|
198
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
199
|
+
const adapter = makeAdapter({
|
|
200
|
+
runMutant: vi
|
|
201
|
+
.fn()
|
|
202
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
203
|
+
});
|
|
204
|
+
const cache = {};
|
|
205
|
+
const task = makeTask({
|
|
206
|
+
key: 'snippet-key',
|
|
207
|
+
v: {
|
|
208
|
+
id: 'source.ts#0',
|
|
209
|
+
name: 'flipArith',
|
|
210
|
+
file: tmpFile,
|
|
211
|
+
code: 'const x = a - b\n',
|
|
212
|
+
line: 1,
|
|
213
|
+
col: 10,
|
|
214
|
+
tests: ['/tests/file.test.ts'],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
await executePool({
|
|
218
|
+
tasks: [task],
|
|
219
|
+
adapter,
|
|
220
|
+
cache,
|
|
221
|
+
concurrency: 1,
|
|
222
|
+
progressMode: 'list',
|
|
223
|
+
cwd: tmpDir,
|
|
224
|
+
});
|
|
225
|
+
expect(cache['snippet-key'].originalSnippet).toBe('const x = a + b');
|
|
226
|
+
expect(cache['snippet-key'].mutatedSnippet).toBe('const x = a - b');
|
|
227
|
+
});
|
|
228
|
+
it('escaped mutant omits snippets when original and mutated lines are identical', async () => {
|
|
229
|
+
const tmpFile = path.join(tmpDir, 'source2.ts');
|
|
230
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
231
|
+
const adapter = makeAdapter({
|
|
232
|
+
runMutant: vi
|
|
233
|
+
.fn()
|
|
234
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
235
|
+
});
|
|
236
|
+
const cache = {};
|
|
237
|
+
const task = makeTask({
|
|
238
|
+
key: 'no-snippet-key',
|
|
239
|
+
v: {
|
|
240
|
+
id: 'source2.ts#0',
|
|
241
|
+
name: 'flipArith',
|
|
242
|
+
file: tmpFile,
|
|
243
|
+
code: 'const x = a + b\n',
|
|
244
|
+
line: 1,
|
|
245
|
+
col: 10,
|
|
246
|
+
tests: ['/tests/file.test.ts'],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
await executePool({
|
|
250
|
+
tasks: [task],
|
|
251
|
+
adapter,
|
|
252
|
+
cache,
|
|
253
|
+
concurrency: 1,
|
|
254
|
+
progressMode: 'list',
|
|
255
|
+
cwd: tmpDir,
|
|
256
|
+
});
|
|
257
|
+
expect(cache['no-snippet-key'].originalSnippet).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
it('escaped mutant omits snippets when file read fails', async () => {
|
|
260
|
+
const adapter = makeAdapter({
|
|
261
|
+
runMutant: vi
|
|
262
|
+
.fn()
|
|
263
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
264
|
+
});
|
|
265
|
+
const cache = {};
|
|
266
|
+
const task = makeTask({
|
|
267
|
+
key: 'missing-file-key',
|
|
268
|
+
v: {
|
|
269
|
+
id: 'missing.ts#0',
|
|
270
|
+
name: 'flipArith',
|
|
271
|
+
file: '/nonexistent/path/missing.ts',
|
|
272
|
+
code: 'const x = a - b\n',
|
|
273
|
+
line: 1,
|
|
274
|
+
col: 10,
|
|
275
|
+
tests: ['/tests/file.test.ts'],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
await executePool({
|
|
279
|
+
tasks: [task],
|
|
280
|
+
adapter,
|
|
281
|
+
cache,
|
|
282
|
+
concurrency: 1,
|
|
283
|
+
progressMode: 'list',
|
|
284
|
+
cwd: tmpDir,
|
|
285
|
+
});
|
|
286
|
+
expect(cache['missing-file-key'].status).toBe('escaped');
|
|
287
|
+
expect(cache['missing-file-key'].originalSnippet).toBeUndefined();
|
|
288
|
+
});
|
|
198
289
|
it('handles adapter errors gracefully and still shuts down', async () => {
|
|
199
290
|
const adapter = makeAdapter({
|
|
200
291
|
runMutant: vi.fn().mockRejectedValue(new Error('adapter failure')),
|
|
@@ -69,7 +69,9 @@ describe('enumerateVariantsForTarget', () => {
|
|
|
69
69
|
it('filters by include', async () => {
|
|
70
70
|
const srcFile = path.join(tmpDir, 'inc.ts');
|
|
71
71
|
await fs.writeFile(srcFile, 'const x = a === b && c || d');
|
|
72
|
-
const result = await enumerateVariantsForTarget(tmpDir, 'inc.ts', [
|
|
72
|
+
const result = await enumerateVariantsForTarget(tmpDir, 'inc.ts', [
|
|
73
|
+
'andToOr',
|
|
74
|
+
]);
|
|
73
75
|
expect(result.every((v) => v.name === 'andToOr')).toBe(true);
|
|
74
76
|
});
|
|
75
77
|
it('filters by exclude', async () => {
|
package/dist/runner/discover.js
CHANGED
|
@@ -85,7 +85,8 @@ async function createViteResolver(rootAbs, exts) {
|
|
|
85
85
|
let plugins = [];
|
|
86
86
|
if (exts.has('.vue')) {
|
|
87
87
|
try {
|
|
88
|
-
const mod = await import(
|
|
88
|
+
const mod = await import(
|
|
89
|
+
/* @vite-ignore */ '@vitejs/plugin-vue');
|
|
89
90
|
const vue = mod.default ?? mod;
|
|
90
91
|
plugins = typeof vue === 'function' ? [vue()] : [];
|
|
91
92
|
}
|
|
@@ -39,7 +39,7 @@ describe('Jest adapter', () => {
|
|
|
39
39
|
afterEach(() => {
|
|
40
40
|
vi.useRealTimers();
|
|
41
41
|
});
|
|
42
|
-
it('
|
|
42
|
+
it('initialises pool with override concurrency', async () => {
|
|
43
43
|
const adapter = makeAdapter();
|
|
44
44
|
await adapter.init(4);
|
|
45
45
|
expect(poolInstance?.init).toHaveBeenCalledTimes(1);
|