@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
package/README.md
CHANGED
|
@@ -26,25 +26,31 @@ Mutations are applied using Babel AST analysis, so operators inside strings and
|
|
|
26
26
|
|
|
27
27
|
## Supported Mutations (WIP)
|
|
28
28
|
|
|
29
|
-
| Category
|
|
30
|
-
|
|
|
31
|
-
| Equality
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
| Boundary
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| Logical
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
| Arithmetic
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
29
|
+
| Category | Mutator | Transformation |
|
|
30
|
+
| ------------ | ------------------- | ------------------------------------ |
|
|
31
|
+
| Equality | `flipStrictEQ` | `===` → `!==` |
|
|
32
|
+
| | `flipStrictNEQ` | `!==` → `===` |
|
|
33
|
+
| | `flipEQ` | `==` → `!=` |
|
|
34
|
+
| | `flipNEQ` | `!=` → `==` |
|
|
35
|
+
| Boundary | `relaxLE` | `<=` → `<` |
|
|
36
|
+
| | `relaxGE` | `>=` → `>` |
|
|
37
|
+
| | `tightenLT` | `<` → `<=` |
|
|
38
|
+
| | `tightenGT` | `>` → `>=` |
|
|
39
|
+
| Logical | `andToOr` | `&&` → `\|\|` |
|
|
40
|
+
| | `orToAnd` | `\|\|` → `&&` |
|
|
41
|
+
| | `nullishToOr` | `??` → `\|\|` |
|
|
42
|
+
| Arithmetic | `addToSub` | `+` → `-` |
|
|
43
|
+
| | `subToAdd` | `-` → `+` |
|
|
44
|
+
| | `mulToDiv` | `*` → `/` |
|
|
45
|
+
| | `divToMul` | `/` → `*` |
|
|
46
|
+
| | `modToMul` | `%` → `*` |
|
|
47
|
+
| | `powerToMul` | `**` → `*` |
|
|
48
|
+
| Return value | `returnToNull` | `return x` → `return null` |
|
|
49
|
+
| | `returnToUndefined` | `return x` → `return undefined` |
|
|
50
|
+
| | `returnFlipBool` | `return true` ↔ `return false` |
|
|
51
|
+
| | `returnZero` | `return n` → `return 0` |
|
|
52
|
+
| | `returnEmptyStr` | `return s` → `return ''` |
|
|
53
|
+
| | `returnEmptyArr` | `return [...]` → `return []` |
|
|
48
54
|
|
|
49
55
|
## Installation
|
|
50
56
|
|
|
@@ -56,11 +62,11 @@ npm i @mutineerjs/mutineer
|
|
|
56
62
|
|
|
57
63
|
### Commands
|
|
58
64
|
|
|
59
|
-
| Command | Description
|
|
60
|
-
| ---------------- |
|
|
61
|
-
| `mutineer init` | Create a `mutineer.config.ts` with minimal defaults
|
|
62
|
-
| `mutineer run` | Run mutation testing
|
|
63
|
-
| `mutineer clean` | Remove leftover `__mutineer__` temp directories
|
|
65
|
+
| Command | Description |
|
|
66
|
+
| ---------------- | --------------------------------------------------- |
|
|
67
|
+
| `mutineer init` | Create a `mutineer.config.ts` with minimal defaults |
|
|
68
|
+
| `mutineer run` | Run mutation testing |
|
|
69
|
+
| `mutineer clean` | Remove leftover `__mutineer__` temp directories |
|
|
64
70
|
|
|
65
71
|
### Quick Start
|
|
66
72
|
|
|
@@ -93,7 +99,7 @@ npm run mutineer
|
|
|
93
99
|
| ------------------------ | ------------------------------------------ | ------------- |
|
|
94
100
|
| `--runner <type>` | Test runner: `vitest` or `jest` | `vitest` |
|
|
95
101
|
| `--config`, `-c` | Path to config file | auto-detected |
|
|
96
|
-
| `--concurrency <n>` | Parallel workers (1
|
|
102
|
+
| `--concurrency <n>` | Parallel workers (min 1) | CPUs - 1 |
|
|
97
103
|
| `--changed` | Only mutate files changed vs base branch | -- |
|
|
98
104
|
| `--changed-with-deps` | Include dependents of changed files | -- |
|
|
99
105
|
| `--only-covered-lines` | Skip mutations on uncovered lines | -- |
|
|
@@ -140,23 +146,23 @@ export default defineMutineerConfig({
|
|
|
140
146
|
|
|
141
147
|
### Config Options
|
|
142
148
|
|
|
143
|
-
| Option | Type
|
|
144
|
-
| ------------------- |
|
|
145
|
-
| `source` | `string \| string[]`
|
|
146
|
-
| `targets` | `MutateTarget[]`
|
|
147
|
-
| `runner` | `'vitest' \| 'jest'`
|
|
148
|
-
| `vitestConfig` | `string`
|
|
149
|
-
| `jestConfig` | `string`
|
|
150
|
-
| `include` | `string[]`
|
|
151
|
-
| `exclude` | `string[]`
|
|
152
|
-
| `excludePaths` | `string[]`
|
|
153
|
-
| `maxMutantsPerFile` | `number`
|
|
154
|
-
| `minKillPercent` | `number`
|
|
155
|
-
| `onlyCoveredLines` | `boolean`
|
|
156
|
-
| `perTestCoverage` | `boolean`
|
|
157
|
-
| `baseRef` | `string`
|
|
158
|
-
| `testPatterns` | `string[]`
|
|
159
|
-
| `extensions` | `string[]`
|
|
149
|
+
| Option | Type | Description |
|
|
150
|
+
| ------------------- | -------------------- | ------------------------------------------------ |
|
|
151
|
+
| `source` | `string \| string[]` | Glob patterns for source files to mutate |
|
|
152
|
+
| `targets` | `MutateTarget[]` | Explicit list of files to mutate |
|
|
153
|
+
| `runner` | `'vitest' \| 'jest'` | Test runner to use |
|
|
154
|
+
| `vitestConfig` | `string` | Path to vitest config |
|
|
155
|
+
| `jestConfig` | `string` | Path to jest config |
|
|
156
|
+
| `include` | `string[]` | Only run these mutators |
|
|
157
|
+
| `exclude` | `string[]` | Skip these mutators |
|
|
158
|
+
| `excludePaths` | `string[]` | Glob patterns for paths to skip |
|
|
159
|
+
| `maxMutantsPerFile` | `number` | Cap mutations per file |
|
|
160
|
+
| `minKillPercent` | `number` | Fail if kill rate is below this |
|
|
161
|
+
| `onlyCoveredLines` | `boolean` | Only mutate lines covered by tests |
|
|
162
|
+
| `perTestCoverage` | `boolean` | Use per-test coverage to select tests |
|
|
163
|
+
| `baseRef` | `string` | Git ref for `--changed` (default: `origin/main`) |
|
|
164
|
+
| `testPatterns` | `string[]` | Globs for test file discovery |
|
|
165
|
+
| `extensions` | `string[]` | File extensions to consider |
|
|
160
166
|
|
|
161
167
|
## File Support
|
|
162
168
|
|
package/dist/bin/mutineer.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { relaxLE, relaxGE, tightenLT, tightenGT, andToOr, orToAnd, nullishToOr, flipEQ, flipNEQ, flipStrictEQ, flipStrictNEQ, addToSub, subToAdd, mulToDiv, divToMul, modToMul, powerToMul, } from '../operator.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Shared behaviour (tested once; all mutators use the same factory)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe('operator mutator shared behaviour', () => {
|
|
7
|
+
it('produces one mutation per matching operator occurrence', () => {
|
|
8
|
+
const src = `const ok = a && b && c`;
|
|
9
|
+
const results = andToOr.apply(src);
|
|
10
|
+
expect(results).toHaveLength(2);
|
|
11
|
+
});
|
|
12
|
+
it('reports the correct 1-based line number', () => {
|
|
13
|
+
const src = `const x = 1\nconst ok = a && b`;
|
|
14
|
+
const [result] = andToOr.apply(src);
|
|
15
|
+
expect(result.line).toBe(2);
|
|
16
|
+
});
|
|
17
|
+
it('reports the correct visual column', () => {
|
|
18
|
+
const src = `const ok = a && b`;
|
|
19
|
+
// ^ col 14 ('a' is at col 12, ' ' 13, '&' 14)
|
|
20
|
+
const [result] = andToOr.apply(src);
|
|
21
|
+
expect(result.col).toBe(14);
|
|
22
|
+
});
|
|
23
|
+
it('does not mutate operators inside string literals (AST-safe)', () => {
|
|
24
|
+
const src = `const s = 'a && b'`;
|
|
25
|
+
const results = andToOr.apply(src);
|
|
26
|
+
expect(results).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
it('does not mutate operators inside comments (AST-safe)', () => {
|
|
29
|
+
const src = `// a && b\nconst x = 1`;
|
|
30
|
+
const results = andToOr.apply(src);
|
|
31
|
+
expect(results).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
it('returns no results when the operator is absent', () => {
|
|
34
|
+
const src = `const x = a || b`;
|
|
35
|
+
const results = andToOr.apply(src);
|
|
36
|
+
expect(results).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Boundary mutators
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
describe('relaxLE', () => {
|
|
43
|
+
it("replaces '<=' with '<'", () => {
|
|
44
|
+
const src = `if (x <= 10) {}`;
|
|
45
|
+
const [result] = relaxLE.apply(src);
|
|
46
|
+
expect(result.code).toBe(`if (x < 10) {}`);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('relaxGE', () => {
|
|
50
|
+
it("replaces '>=' with '>'", () => {
|
|
51
|
+
const src = `if (x >= 0) {}`;
|
|
52
|
+
const [result] = relaxGE.apply(src);
|
|
53
|
+
expect(result.code).toBe(`if (x > 0) {}`);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('tightenLT', () => {
|
|
57
|
+
it("replaces '<' with '<='", () => {
|
|
58
|
+
const src = `if (x < 10) {}`;
|
|
59
|
+
const [result] = tightenLT.apply(src);
|
|
60
|
+
expect(result.code).toBe(`if (x <= 10) {}`);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('tightenGT', () => {
|
|
64
|
+
it("replaces '>' with '>='", () => {
|
|
65
|
+
const src = `if (x > 0) {}`;
|
|
66
|
+
const [result] = tightenGT.apply(src);
|
|
67
|
+
expect(result.code).toBe(`if (x >= 0) {}`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Logical mutators
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
describe('andToOr', () => {
|
|
74
|
+
it("replaces '&&' with '||'", () => {
|
|
75
|
+
const src = `const ok = a && b`;
|
|
76
|
+
const [result] = andToOr.apply(src);
|
|
77
|
+
expect(result.code).toBe(`const ok = a || b`);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('orToAnd', () => {
|
|
81
|
+
it("replaces '||' with '&&'", () => {
|
|
82
|
+
const src = `const ok = a || b`;
|
|
83
|
+
const [result] = orToAnd.apply(src);
|
|
84
|
+
expect(result.code).toBe(`const ok = a && b`);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('nullishToOr', () => {
|
|
88
|
+
it("replaces '??' with '||'", () => {
|
|
89
|
+
const src = `const v = x ?? defaultValue`;
|
|
90
|
+
const [result] = nullishToOr.apply(src);
|
|
91
|
+
expect(result.code).toBe(`const v = x || defaultValue`);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Equality mutators
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe('flipEQ', () => {
|
|
98
|
+
it("replaces '==' with '!='", () => {
|
|
99
|
+
const src = `if (a == b) {}`;
|
|
100
|
+
const [result] = flipEQ.apply(src);
|
|
101
|
+
expect(result.code).toBe(`if (a != b) {}`);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('flipNEQ', () => {
|
|
105
|
+
it("replaces '!=' with '=='", () => {
|
|
106
|
+
const src = `if (a != b) {}`;
|
|
107
|
+
const [result] = flipNEQ.apply(src);
|
|
108
|
+
expect(result.code).toBe(`if (a == b) {}`);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('flipStrictEQ', () => {
|
|
112
|
+
it("replaces '===' with '!=='", () => {
|
|
113
|
+
const src = `if (a === b) {}`;
|
|
114
|
+
const [result] = flipStrictEQ.apply(src);
|
|
115
|
+
expect(result.code).toBe(`if (a !== b) {}`);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('flipStrictNEQ', () => {
|
|
119
|
+
it("replaces '!==' with '==='", () => {
|
|
120
|
+
const src = `if (a !== b) {}`;
|
|
121
|
+
const [result] = flipStrictNEQ.apply(src);
|
|
122
|
+
expect(result.code).toBe(`if (a === b) {}`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Arithmetic mutators
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
describe('addToSub', () => {
|
|
129
|
+
it("replaces '+' with '-'", () => {
|
|
130
|
+
const src = `const n = a + b`;
|
|
131
|
+
const [result] = addToSub.apply(src);
|
|
132
|
+
expect(result.code).toBe(`const n = a - b`);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('subToAdd', () => {
|
|
136
|
+
it("replaces '-' with '+'", () => {
|
|
137
|
+
const src = `const n = a - b`;
|
|
138
|
+
const [result] = subToAdd.apply(src);
|
|
139
|
+
expect(result.code).toBe(`const n = a + b`);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('mulToDiv', () => {
|
|
143
|
+
it("replaces '*' with '/'", () => {
|
|
144
|
+
const src = `const n = a * b`;
|
|
145
|
+
const [result] = mulToDiv.apply(src);
|
|
146
|
+
expect(result.code).toBe(`const n = a / b`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('divToMul', () => {
|
|
150
|
+
it("replaces '/' with '*'", () => {
|
|
151
|
+
const src = `const n = a / b`;
|
|
152
|
+
const [result] = divToMul.apply(src);
|
|
153
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('modToMul', () => {
|
|
157
|
+
it("replaces '%' with '*'", () => {
|
|
158
|
+
const src = `const n = a % b`;
|
|
159
|
+
const [result] = modToMul.apply(src);
|
|
160
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('powerToMul', () => {
|
|
164
|
+
it("replaces '**' with '*'", () => {
|
|
165
|
+
const src = `const n = a ** b`;
|
|
166
|
+
const [result] = powerToMul.apply(src);
|
|
167
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -18,6 +18,12 @@ const ALL_NAMES = [
|
|
|
18
18
|
'divToMul',
|
|
19
19
|
'modToMul',
|
|
20
20
|
'powerToMul',
|
|
21
|
+
'returnToNull',
|
|
22
|
+
'returnToUndefined',
|
|
23
|
+
'returnFlipBool',
|
|
24
|
+
'returnZero',
|
|
25
|
+
'returnEmptyStr',
|
|
26
|
+
'returnEmptyArr',
|
|
21
27
|
];
|
|
22
28
|
describe('mutator registry', () => {
|
|
23
29
|
it('returns all mutators by default in declared order', () => {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { returnToNull, returnToUndefined, returnFlipBool, returnZero, returnEmptyStr, returnEmptyArr, } from '../return-value.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// returnToNull
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe('returnToNull', () => {
|
|
7
|
+
it('replaces an identifier return with null', () => {
|
|
8
|
+
const src = `function f() { return x }`;
|
|
9
|
+
const [mutation] = returnToNull.apply(src);
|
|
10
|
+
expect(mutation.code).toBe(`function f() { return null }`);
|
|
11
|
+
});
|
|
12
|
+
it('replaces a function call return with null', () => {
|
|
13
|
+
const src = `function f() { return getValue() }`;
|
|
14
|
+
const [mutation] = returnToNull.apply(src);
|
|
15
|
+
expect(mutation.code).toBe(`function f() { return null }`);
|
|
16
|
+
});
|
|
17
|
+
it('replaces a numeric literal return with null', () => {
|
|
18
|
+
const src = `function f() { return 42 }`;
|
|
19
|
+
const [mutation] = returnToNull.apply(src);
|
|
20
|
+
expect(mutation.code).toBe(`function f() { return null }`);
|
|
21
|
+
});
|
|
22
|
+
it('replaces an object expression return with null', () => {
|
|
23
|
+
const src = `function f() { return { a: 1 } }`;
|
|
24
|
+
const [mutation] = returnToNull.apply(src);
|
|
25
|
+
expect(mutation.code).toBe(`function f() { return null }`);
|
|
26
|
+
});
|
|
27
|
+
it('does not mutate bare return;', () => {
|
|
28
|
+
const src = `function f() { return }`;
|
|
29
|
+
const results = returnToNull.apply(src);
|
|
30
|
+
expect(results).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
it('does not mutate return null', () => {
|
|
33
|
+
const src = `function f() { return null }`;
|
|
34
|
+
const results = returnToNull.apply(src);
|
|
35
|
+
expect(results).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('produces one mutation per matching return statement', () => {
|
|
38
|
+
const src = `
|
|
39
|
+
function f() {
|
|
40
|
+
if (x) return a
|
|
41
|
+
return b
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
const results = returnToNull.apply(src);
|
|
45
|
+
expect(results).toHaveLength(2);
|
|
46
|
+
expect(results[0].code).toContain('return null');
|
|
47
|
+
expect(results[1].code).toContain('return null');
|
|
48
|
+
});
|
|
49
|
+
it('reports the correct line for the return statement', () => {
|
|
50
|
+
const src = `function f() {\n return x\n}`;
|
|
51
|
+
const [mutation] = returnToNull.apply(src);
|
|
52
|
+
expect(mutation.line).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
it('respects mutineer-disable-next-line', () => {
|
|
55
|
+
const src = `
|
|
56
|
+
function f() {
|
|
57
|
+
// mutineer-disable-next-line
|
|
58
|
+
return x
|
|
59
|
+
return y
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const results = returnToNull.apply(src);
|
|
63
|
+
expect(results).toHaveLength(1);
|
|
64
|
+
expect(results[0].code).toContain('return null');
|
|
65
|
+
});
|
|
66
|
+
it('respects mutineer-disable-line', () => {
|
|
67
|
+
const src = `
|
|
68
|
+
function f() {
|
|
69
|
+
return x // mutineer-disable-line
|
|
70
|
+
return y
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
const results = returnToNull.apply(src);
|
|
74
|
+
expect(results).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// returnToUndefined
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
describe('returnToUndefined', () => {
|
|
81
|
+
it('replaces an identifier return with undefined', () => {
|
|
82
|
+
const src = `function f() { return x }`;
|
|
83
|
+
const [mutation] = returnToUndefined.apply(src);
|
|
84
|
+
expect(mutation.code).toBe(`function f() { return undefined }`);
|
|
85
|
+
});
|
|
86
|
+
it('replaces a function call return with undefined', () => {
|
|
87
|
+
const src = `function f() { return getValue() }`;
|
|
88
|
+
const [mutation] = returnToUndefined.apply(src);
|
|
89
|
+
expect(mutation.code).toBe(`function f() { return undefined }`);
|
|
90
|
+
});
|
|
91
|
+
it('replaces null with undefined', () => {
|
|
92
|
+
const src = `function f() { return null }`;
|
|
93
|
+
const [mutation] = returnToUndefined.apply(src);
|
|
94
|
+
expect(mutation.code).toBe(`function f() { return undefined }`);
|
|
95
|
+
});
|
|
96
|
+
it('does not mutate bare return;', () => {
|
|
97
|
+
const src = `function f() { return }`;
|
|
98
|
+
const results = returnToUndefined.apply(src);
|
|
99
|
+
expect(results).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
it('does not mutate return undefined', () => {
|
|
102
|
+
const src = `function f() { return undefined }`;
|
|
103
|
+
const results = returnToUndefined.apply(src);
|
|
104
|
+
expect(results).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// returnFlipBool
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
describe('returnFlipBool', () => {
|
|
111
|
+
it('flips return true to return false', () => {
|
|
112
|
+
const src = `function f() { return true }`;
|
|
113
|
+
const [mutation] = returnFlipBool.apply(src);
|
|
114
|
+
expect(mutation.code).toBe(`function f() { return false }`);
|
|
115
|
+
});
|
|
116
|
+
it('flips return false to return true', () => {
|
|
117
|
+
const src = `function f() { return false }`;
|
|
118
|
+
const [mutation] = returnFlipBool.apply(src);
|
|
119
|
+
expect(mutation.code).toBe(`function f() { return true }`);
|
|
120
|
+
});
|
|
121
|
+
it('does not mutate non-boolean returns', () => {
|
|
122
|
+
const src = `function f() { return x }`;
|
|
123
|
+
const results = returnFlipBool.apply(src);
|
|
124
|
+
expect(results).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
it('does not mutate bare return;', () => {
|
|
127
|
+
const src = `function f() { return }`;
|
|
128
|
+
const results = returnFlipBool.apply(src);
|
|
129
|
+
expect(results).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
it('handles multiple boolean returns independently', () => {
|
|
132
|
+
const src = `
|
|
133
|
+
function f(x) {
|
|
134
|
+
if (x) return true
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
const results = returnFlipBool.apply(src);
|
|
139
|
+
expect(results).toHaveLength(2);
|
|
140
|
+
expect(results[0].code).toContain('return false');
|
|
141
|
+
expect(results[1].code).toContain('return true');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// returnZero
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
describe('returnZero', () => {
|
|
148
|
+
it('replaces a positive integer return with 0', () => {
|
|
149
|
+
const src = `function f() { return 42 }`;
|
|
150
|
+
const [mutation] = returnZero.apply(src);
|
|
151
|
+
expect(mutation.code).toBe(`function f() { return 0 }`);
|
|
152
|
+
});
|
|
153
|
+
it('replaces a negative number return with 0', () => {
|
|
154
|
+
const src = `function f() { return -1 }`;
|
|
155
|
+
// -1 is a UnaryExpression, not a NumericLiteral — should produce no output
|
|
156
|
+
const results = returnZero.apply(src);
|
|
157
|
+
expect(results).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
it('replaces a float return with 0', () => {
|
|
160
|
+
const src = `function f() { return 3.14 }`;
|
|
161
|
+
const [mutation] = returnZero.apply(src);
|
|
162
|
+
expect(mutation.code).toBe(`function f() { return 0 }`);
|
|
163
|
+
});
|
|
164
|
+
it('does not mutate return 0', () => {
|
|
165
|
+
const src = `function f() { return 0 }`;
|
|
166
|
+
const results = returnZero.apply(src);
|
|
167
|
+
expect(results).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
it('does not mutate non-numeric returns', () => {
|
|
170
|
+
const src = `function f() { return x }`;
|
|
171
|
+
const results = returnZero.apply(src);
|
|
172
|
+
expect(results).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
it('does not mutate bare return;', () => {
|
|
175
|
+
const src = `function f() { return }`;
|
|
176
|
+
const results = returnZero.apply(src);
|
|
177
|
+
expect(results).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// returnEmptyStr
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
describe('returnEmptyStr', () => {
|
|
184
|
+
it("replaces a non-empty string return with ''", () => {
|
|
185
|
+
const src = `function f() { return 'hello' }`;
|
|
186
|
+
const [mutation] = returnEmptyStr.apply(src);
|
|
187
|
+
expect(mutation.code).toBe(`function f() { return '' }`);
|
|
188
|
+
});
|
|
189
|
+
it('replaces a double-quoted string return', () => {
|
|
190
|
+
const src = `function f() { return "world" }`;
|
|
191
|
+
const [mutation] = returnEmptyStr.apply(src);
|
|
192
|
+
expect(mutation.code).toBe(`function f() { return '' }`);
|
|
193
|
+
});
|
|
194
|
+
it("does not mutate return ''", () => {
|
|
195
|
+
const src = `function f() { return '' }`;
|
|
196
|
+
const results = returnEmptyStr.apply(src);
|
|
197
|
+
expect(results).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
it('does not mutate non-string returns', () => {
|
|
200
|
+
const src = `function f() { return x }`;
|
|
201
|
+
const results = returnEmptyStr.apply(src);
|
|
202
|
+
expect(results).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
it('does not mutate bare return;', () => {
|
|
205
|
+
const src = `function f() { return }`;
|
|
206
|
+
const results = returnEmptyStr.apply(src);
|
|
207
|
+
expect(results).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// returnEmptyArr
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
describe('returnEmptyArr', () => {
|
|
214
|
+
it('replaces a non-empty array return with []', () => {
|
|
215
|
+
const src = `function f() { return [1, 2, 3] }`;
|
|
216
|
+
const [mutation] = returnEmptyArr.apply(src);
|
|
217
|
+
expect(mutation.code).toBe(`function f() { return [] }`);
|
|
218
|
+
});
|
|
219
|
+
it('replaces a single-element array return with []', () => {
|
|
220
|
+
const src = `function f() { return [x] }`;
|
|
221
|
+
const [mutation] = returnEmptyArr.apply(src);
|
|
222
|
+
expect(mutation.code).toBe(`function f() { return [] }`);
|
|
223
|
+
});
|
|
224
|
+
it('does not mutate return []', () => {
|
|
225
|
+
const src = `function f() { return [] }`;
|
|
226
|
+
const results = returnEmptyArr.apply(src);
|
|
227
|
+
expect(results).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
it('does not mutate non-array returns', () => {
|
|
230
|
+
const src = `function f() { return x }`;
|
|
231
|
+
const results = returnEmptyArr.apply(src);
|
|
232
|
+
expect(results).toHaveLength(0);
|
|
233
|
+
});
|
|
234
|
+
it('does not mutate bare return;', () => {
|
|
235
|
+
const src = `function f() { return }`;
|
|
236
|
+
const results = returnEmptyArr.apply(src);
|
|
237
|
+
expect(results).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { collectOperatorTargets } from '../utils.js';
|
|
2
|
+
import { collectOperatorTargets, buildIgnoreLines, parseSource, } from '../utils.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// buildIgnoreLines
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe('buildIgnoreLines', () => {
|
|
7
|
+
function comment(text, startLine, endLine = startLine) {
|
|
8
|
+
return {
|
|
9
|
+
type: 'CommentLine',
|
|
10
|
+
value: ` ${text}`,
|
|
11
|
+
start: 0,
|
|
12
|
+
end: 0,
|
|
13
|
+
loc: {
|
|
14
|
+
start: { line: startLine, column: 0, index: 0 },
|
|
15
|
+
end: { line: endLine, column: 0, index: 0 },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
it('ignores the line after mutineer-disable-next-line', () => {
|
|
20
|
+
// The text also contains 'mutineer-disable' as a substring, so the
|
|
21
|
+
// comment line itself is ignored in addition to the following line.
|
|
22
|
+
const lines = buildIgnoreLines([comment('mutineer-disable-next-line', 3)]);
|
|
23
|
+
expect(lines.has(4)).toBe(true);
|
|
24
|
+
expect(lines.has(3)).toBe(true);
|
|
25
|
+
expect(lines.has(5)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('ignores the comment line itself for mutineer-disable-line', () => {
|
|
28
|
+
const lines = buildIgnoreLines([comment('mutineer-disable-line', 5)]);
|
|
29
|
+
expect(lines.has(5)).toBe(true);
|
|
30
|
+
expect(lines.has(6)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
it('ignores all lines spanned by a mutineer-disable block comment', () => {
|
|
33
|
+
const lines = buildIgnoreLines([comment('mutineer-disable', 2, 4)]);
|
|
34
|
+
expect(lines.has(2)).toBe(true);
|
|
35
|
+
expect(lines.has(3)).toBe(true);
|
|
36
|
+
expect(lines.has(4)).toBe(true);
|
|
37
|
+
expect(lines.has(1)).toBe(false);
|
|
38
|
+
expect(lines.has(5)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('ignores comments without loc', () => {
|
|
41
|
+
const c = {
|
|
42
|
+
...comment('mutineer-disable-next-line', 1),
|
|
43
|
+
loc: undefined,
|
|
44
|
+
};
|
|
45
|
+
expect(() => buildIgnoreLines([c])).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
it('returns an empty set when there are no disable comments', () => {
|
|
48
|
+
const lines = buildIgnoreLines([comment('just a normal comment', 1)]);
|
|
49
|
+
expect(lines.size).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// parseSource
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
describe('parseSource', () => {
|
|
56
|
+
it('parses valid TypeScript source without throwing', () => {
|
|
57
|
+
expect(() => parseSource(`const x: number = 1`)).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
it('parses JSX without throwing', () => {
|
|
60
|
+
expect(() => parseSource(`const el = <div />`)).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
it('returns a File node', () => {
|
|
63
|
+
const ast = parseSource(`const x = 1`);
|
|
64
|
+
expect(ast.type).toBe('File');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// collectOperatorTargets
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
3
70
|
describe('collectOperatorTargets', () => {
|
|
4
71
|
it('honors mutineer disable comments', () => {
|
|
5
72
|
const src = `// mutineer-disable-next-line
|
|
@@ -0,0 +1,25 @@
|
|
|
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 type { ASTMutator } from './types.js';
|
|
9
|
+
export declare const relaxLE: ASTMutator;
|
|
10
|
+
export declare const relaxGE: ASTMutator;
|
|
11
|
+
export declare const tightenLT: ASTMutator;
|
|
12
|
+
export declare const tightenGT: ASTMutator;
|
|
13
|
+
export declare const andToOr: ASTMutator;
|
|
14
|
+
export declare const orToAnd: ASTMutator;
|
|
15
|
+
export declare const nullishToOr: ASTMutator;
|
|
16
|
+
export declare const flipEQ: ASTMutator;
|
|
17
|
+
export declare const flipNEQ: ASTMutator;
|
|
18
|
+
export declare const flipStrictEQ: ASTMutator;
|
|
19
|
+
export declare const flipStrictNEQ: ASTMutator;
|
|
20
|
+
export declare const addToSub: ASTMutator;
|
|
21
|
+
export declare const subToAdd: ASTMutator;
|
|
22
|
+
export declare const mulToDiv: ASTMutator;
|
|
23
|
+
export declare const divToMul: ASTMutator;
|
|
24
|
+
export declare const modToMul: ASTMutator;
|
|
25
|
+
export declare const powerToMul: ASTMutator;
|