@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.
Files changed (135) hide show
  1. package/README.md +48 -42
  2. package/dist/bin/mutineer.js +0 -0
  3. package/dist/mutators/__tests__/operator.spec.js +169 -0
  4. package/dist/mutators/__tests__/registry.spec.js +6 -0
  5. package/dist/mutators/__tests__/return-value.spec.js +239 -0
  6. package/dist/mutators/__tests__/utils.spec.js +68 -1
  7. package/dist/mutators/operator.d.ts +25 -0
  8. package/dist/mutators/operator.js +50 -0
  9. package/dist/mutators/registry.d.ts +6 -28
  10. package/dist/mutators/registry.js +14 -66
  11. package/dist/mutators/return-value.d.ts +39 -0
  12. package/dist/mutators/return-value.js +104 -0
  13. package/dist/mutators/utils.d.ts +21 -0
  14. package/dist/mutators/utils.js +44 -27
  15. package/dist/runner/__tests__/pool-executor.spec.js +95 -4
  16. package/dist/runner/__tests__/variants.spec.js +3 -1
  17. package/dist/runner/discover.js +2 -1
  18. package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
  19. package/dist/runner/jest/__tests__/pool.spec.js +5 -6
  20. package/dist/runner/jest/__tests__/worker-runtime.spec.js +2 -2
  21. package/dist/runner/jest/adapter.js +1 -1
  22. package/dist/runner/jest/pool.d.ts +1 -1
  23. package/dist/runner/jest/pool.js +6 -6
  24. package/dist/runner/jest/worker.mjs +1 -1
  25. package/dist/runner/pool-executor.js +21 -1
  26. package/dist/runner/shared/__tests__/redirect-state.spec.js +3 -3
  27. package/dist/runner/shared/index.d.ts +1 -1
  28. package/dist/runner/shared/index.js +1 -1
  29. package/dist/runner/shared/redirect-state.d.ts +2 -2
  30. package/dist/runner/shared/redirect-state.js +4 -4
  31. package/dist/runner/types.d.ts +1 -1
  32. package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
  33. package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
  34. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +83 -1
  35. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
  36. package/dist/runner/vitest/adapter.js +1 -1
  37. package/dist/runner/vitest/index.d.ts +0 -1
  38. package/dist/runner/vitest/index.js +0 -1
  39. package/dist/runner/vitest/pool.d.ts +1 -1
  40. package/dist/runner/vitest/pool.js +7 -7
  41. package/dist/runner/vitest/redirect-loader.d.ts +1 -1
  42. package/dist/runner/vitest/redirect-loader.js +1 -1
  43. package/dist/runner/vitest/worker-runtime.js +3 -3
  44. package/dist/runner/vitest/worker.mjs +1 -1
  45. package/dist/types/mutant.d.ts +2 -0
  46. package/dist/utils/__tests__/coverage.spec.js +167 -0
  47. package/dist/utils/__tests__/progress.spec.js +96 -0
  48. package/dist/utils/__tests__/summary.spec.js +28 -0
  49. package/dist/utils/summary.js +7 -1
  50. package/package.json +71 -22
  51. package/dist/admin/assets/index-B7nXq-e7.js +0 -32
  52. package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
  53. package/dist/admin/assets/index-BDQLkBUE.js +0 -32
  54. package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
  55. package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
  56. package/dist/admin/index.html +0 -13
  57. package/dist/admin/server/admin.d.ts +0 -6
  58. package/dist/admin/server/admin.js +0 -234
  59. package/dist/bin/mutate-vitest.d.ts +0 -2
  60. package/dist/bin/mutate-vitest.js +0 -90
  61. package/dist/plugin/viteMutate.d.ts +0 -15
  62. package/dist/plugin/viteMutate.js +0 -52
  63. package/dist/plugin/vitest.setup.d.ts +0 -47
  64. package/dist/plugin/vitest.setup.js +0 -118
  65. package/dist/plugin/withVitest.d.ts +0 -13
  66. package/dist/plugin/withVitest.js +0 -30
  67. package/dist/runner/__tests__/orchestrator.spec.js +0 -55
  68. package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
  69. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +0 -1
  70. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
  71. package/dist/runner/adapters/__tests__/vitest.spec.d.ts +0 -1
  72. package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
  73. package/dist/runner/adapters/index.d.ts +0 -10
  74. package/dist/runner/adapters/index.js +0 -9
  75. package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +0 -1
  76. package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
  77. package/dist/runner/adapters/jest/index.d.ts +0 -24
  78. package/dist/runner/adapters/jest/index.js +0 -216
  79. package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
  80. package/dist/runner/adapters/jest/worker-runtime.js +0 -171
  81. package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
  82. package/dist/runner/adapters/jest-worker-runtime.js +0 -171
  83. package/dist/runner/adapters/jest.d.ts +0 -24
  84. package/dist/runner/adapters/jest.js +0 -216
  85. package/dist/runner/adapters/types.d.ts +0 -89
  86. package/dist/runner/adapters/types.js +0 -8
  87. package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +0 -1
  88. package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
  89. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +0 -1
  90. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
  91. package/dist/runner/adapters/vitest/index.d.ts +0 -33
  92. package/dist/runner/adapters/vitest/index.js +0 -267
  93. package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
  94. package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
  95. package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
  96. package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
  97. package/dist/runner/adapters/vitest.d.ts +0 -33
  98. package/dist/runner/adapters/vitest.js +0 -267
  99. package/dist/runner/pool/__tests__/index.spec.d.ts +0 -1
  100. package/dist/runner/pool/__tests__/index.spec.js +0 -83
  101. package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +0 -1
  102. package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
  103. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +0 -1
  104. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
  105. package/dist/runner/pool/index.d.ts +0 -8
  106. package/dist/runner/pool/index.js +0 -9
  107. package/dist/runner/pool/jest/pool.d.ts +0 -52
  108. package/dist/runner/pool/jest/pool.js +0 -309
  109. package/dist/runner/pool/jest/worker.d.mts +0 -1
  110. package/dist/runner/pool/jest/worker.mjs +0 -60
  111. package/dist/runner/pool/jest-pool.d.ts +0 -52
  112. package/dist/runner/pool/jest-pool.js +0 -309
  113. package/dist/runner/pool/jest-worker.d.mts +0 -1
  114. package/dist/runner/pool/jest-worker.mjs +0 -60
  115. package/dist/runner/pool/plugin.d.ts +0 -18
  116. package/dist/runner/pool/plugin.js +0 -60
  117. package/dist/runner/pool/pool-plugin.d.ts +0 -18
  118. package/dist/runner/pool/pool-plugin.js +0 -60
  119. package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
  120. package/dist/runner/pool/pool-redirect-loader.js +0 -116
  121. package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
  122. package/dist/runner/pool/redirect-loader.d.ts +0 -19
  123. package/dist/runner/pool/redirect-loader.js +0 -116
  124. package/dist/runner/pool/vitest/pool.d.ts +0 -70
  125. package/dist/runner/pool/vitest/pool.js +0 -376
  126. package/dist/runner/pool/vitest/worker.d.mts +0 -15
  127. package/dist/runner/pool/vitest/worker.mjs +0 -96
  128. package/dist/runner/pool/vitest-worker.d.mts +0 -15
  129. package/dist/runner/pool/vitest-worker.mjs +0 -96
  130. package/dist/runner/shared-module-redirect.d.ts +0 -56
  131. package/dist/runner/shared-module-redirect.js +0 -84
  132. package/dist/types/api.d.ts +0 -20
  133. package/dist/types/api.js +0 -1
  134. /package/dist/{runner/__tests__/orchestrator.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
  135. /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 | Mutator | Transformation |
30
- | ---------- | --------------- | ------------------ |
31
- | Equality | `flipStrictEQ` | `===` → `!==` |
32
- | | `flipStrictNEQ` | `!==` → `===` |
33
- | | `flipEQ` | `==` → `!=` |
34
- | | `flipNEQ` | `!=` → `==` |
35
- | Boundary | `relaxLE` | `<=` &rarr; `<` |
36
- | | `relaxGE` | `>=` &rarr; `>` |
37
- | | `tightenLT` | `<` &rarr; `<=` |
38
- | | `tightenGT` | `>` &rarr; `>=` |
39
- | Logical | `andToOr` | `&&` &rarr; `\|\|` |
40
- | | `orToAnd` | `\|\|` &rarr; `&&` |
41
- | | `nullishToOr` | `??` &rarr; `\|\|` |
42
- | Arithmetic | `addToSub` | `+` &rarr; `-` |
43
- | | `subToAdd` | `-` &rarr; `+` |
44
- | | `mulToDiv` | `*` &rarr; `/` |
45
- | | `divToMul` | `/` &rarr; `*` |
46
- | | `modToMul` | `%` &rarr; `*` |
47
- | | `powerToMul` | `**` &rarr; `*` |
29
+ | Category | Mutator | Transformation |
30
+ | ------------ | ------------------- | ------------------------------------ |
31
+ | Equality | `flipStrictEQ` | `===` &rarr; `!==` |
32
+ | | `flipStrictNEQ` | `!==` &rarr; `===` |
33
+ | | `flipEQ` | `==` &rarr; `!=` |
34
+ | | `flipNEQ` | `!=` &rarr; `==` |
35
+ | Boundary | `relaxLE` | `<=` &rarr; `<` |
36
+ | | `relaxGE` | `>=` &rarr; `>` |
37
+ | | `tightenLT` | `<` &rarr; `<=` |
38
+ | | `tightenGT` | `>` &rarr; `>=` |
39
+ | Logical | `andToOr` | `&&` &rarr; `\|\|` |
40
+ | | `orToAnd` | `\|\|` &rarr; `&&` |
41
+ | | `nullishToOr` | `??` &rarr; `\|\|` |
42
+ | Arithmetic | `addToSub` | `+` &rarr; `-` |
43
+ | | `subToAdd` | `-` &rarr; `+` |
44
+ | | `mulToDiv` | `*` &rarr; `/` |
45
+ | | `divToMul` | `/` &rarr; `*` |
46
+ | | `modToMul` | `%` &rarr; `*` |
47
+ | | `powerToMul` | `**` &rarr; `*` |
48
+ | Return value | `returnToNull` | `return x` &rarr; `return null` |
49
+ | | `returnToUndefined` | `return x` &rarr; `return undefined` |
50
+ | | `returnFlipBool` | `return true` &harr; `return false` |
51
+ | | `returnZero` | `return n` &rarr; `return 0` |
52
+ | | `returnEmptyStr` | `return s` &rarr; `return ''` |
53
+ | | `returnEmptyArr` | `return [...]` &rarr; `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-4) | CPUs - 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 | Description |
144
- | ------------------- | ---------------------------------- | ------------------------------------------------ |
145
- | `source` | `string \| string[]` | Glob patterns for source files to mutate |
146
- | `targets` | `MutateTarget[]` | Explicit list of files to mutate |
147
- | `runner` | `'vitest' \| 'jest'` | Test runner to use |
148
- | `vitestConfig` | `string` | Path to vitest config |
149
- | `jestConfig` | `string` | Path to jest config |
150
- | `include` | `string[]` | Only run these mutators |
151
- | `exclude` | `string[]` | Skip these mutators |
152
- | `excludePaths` | `string[]` | Glob patterns for paths to skip |
153
- | `maxMutantsPerFile` | `number` | Cap mutations per file |
154
- | `minKillPercent` | `number` | Fail if kill rate is below this |
155
- | `onlyCoveredLines` | `boolean` | Only mutate lines covered by tests |
156
- | `perTestCoverage` | `boolean` | Use per-test coverage to select tests |
157
- | `baseRef` | `string` | Git ref for `--changed` (default: `origin/main`) |
158
- | `testPatterns` | `string[]` | Globs for test file discovery |
159
- | `extensions` | `string[]` | File extensions to consider |
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
 
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;