@systemfsoftware/stryker-js-typescript-checker 1.2.2 → 1.3.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/CHANGELOG.md +33 -0
- package/LICENSE +203 -21
- package/README.md +31 -0
- package/dist/index.d.mts +19 -19
- package/dist/index.mjs +132 -122
- package/package.json +23 -10
- package/.turbo/turbo-build.log +0 -21
- package/AGENTS.md +0 -7
- package/oxlint.config.ts +0 -9
- package/src/fs/hybrid-file-system.ts +0 -118
- package/src/fs/index.ts +0 -2
- package/src/fs/script-file.ts +0 -44
- package/src/grouping/create-groups.ts +0 -77
- package/src/grouping/ts-file-node.ts +0 -54
- package/src/index.ts +0 -18
- package/src/plugin-tokens.ts +0 -2
- package/src/tsconfig-helpers.ts +0 -187
- package/src/typescript-checker-options-with-stryker-options.ts +0 -9
- package/src/typescript-checker.ts +0 -223
- package/src/typescript-compiler.ts +0 -412
- package/test/integration/e2e-plugin-entry.it.spec.ts +0 -153
- package/test/integration/project-references.it.spec.ts +0 -146
- package/test/integration/project-with-ts-buildinfo.it.spec.ts +0 -92
- package/test/integration/single-project.it.spec.ts +0 -298
- package/test/integration/tsconfig-helpers.it.spec.ts +0 -199
- package/test/integration/typescript-checkers-errors.it.spec.ts +0 -112
- package/test/unit/fs/hybrid-file-system.spec.ts +0 -109
- package/test/unit/grouping/create-groups.spec.ts +0 -122
- package/test/unit/grouping/ts-file-node.spec.ts +0 -132
- package/testResources/errors/compile-error/add.ts +0 -3
- package/testResources/errors/compile-error/tsconfig.json +0 -6
- package/testResources/errors/empty-dir/.gitkeep +0 -0
- package/testResources/errors/invalid-tsconfig/tsconfig.json +0 -1
- package/testResources/project-references/src/index.ts +0 -5
- package/testResources/project-references/src/job.ts +0 -6
- package/testResources/project-references/src/tsconfig.json +0 -10
- package/testResources/project-references/tsconfig.root.json +0 -7
- package/testResources/project-references/tsconfig.settings.json +0 -17
- package/testResources/project-references/utils/math.ts +0 -3
- package/testResources/project-references/utils/text.ts +0 -3
- package/testResources/project-references/utils/tsconfig.json +0 -7
- package/testResources/project-with-ts-buildinfo/src/index.ts +0 -3
- package/testResources/project-with-ts-buildinfo/tsconfig.json +0 -17
- package/testResources/single-project/src/counter.ts +0 -9
- package/testResources/single-project/src/errorInFileAbove2Mutants/counter.ts +0 -9
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo-counter.ts +0 -7
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo.spec.ts +0 -11
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo.ts +0 -22
- package/testResources/single-project/src/not-type-checked.js +0 -1
- package/testResources/single-project/src/todo.spec.ts +0 -11
- package/testResources/single-project/src/todo.ts +0 -22
- package/testResources/single-project/tsconfig.json +0 -17
- package/tsconfig.json +0 -26
- package/tsdown.config.ts +0 -16
- package/vitest.config.ts +0 -13
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
|
|
5
|
-
import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
6
|
-
import type { Logger } from '@stryker-mutator/api/logging'
|
|
7
|
-
import { describe, expect, it } from 'vitest'
|
|
8
|
-
|
|
9
|
-
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
10
|
-
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
11
|
-
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
12
|
-
|
|
13
|
-
const resolveTestResource = path.resolve.bind(
|
|
14
|
-
path,
|
|
15
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
16
|
-
'..',
|
|
17
|
-
'..',
|
|
18
|
-
'testResources',
|
|
19
|
-
'project-with-ts-buildinfo',
|
|
20
|
-
) as unknown as typeof path.resolve
|
|
21
|
-
|
|
22
|
-
function createLogger(): Logger {
|
|
23
|
-
return {
|
|
24
|
-
isTraceEnabled: () => false,
|
|
25
|
-
isDebugEnabled: () => false,
|
|
26
|
-
isInfoEnabled: () => false,
|
|
27
|
-
isWarnEnabled: () => false,
|
|
28
|
-
isErrorEnabled: () => false,
|
|
29
|
-
isFatalEnabled: () => false,
|
|
30
|
-
trace: () => {},
|
|
31
|
-
debug: () => {},
|
|
32
|
-
info: () => {},
|
|
33
|
-
warn: () => {},
|
|
34
|
-
error: () => {},
|
|
35
|
-
fatal: () => {},
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
40
|
-
const options = {
|
|
41
|
-
tsconfigFile,
|
|
42
|
-
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
43
|
-
} as unknown as StrykerOptions
|
|
44
|
-
const logger = createLogger()
|
|
45
|
-
const fileSystem = new HybridFileSystem()
|
|
46
|
-
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
47
|
-
return new TypescriptChecker(logger, options, compiler)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('project-with-ts-buildinfo', () => {
|
|
51
|
-
it('should load project on init', async () => {
|
|
52
|
-
const sut = createChecker(resolveTestResource('tsconfig.json'))
|
|
53
|
-
await sut.init()
|
|
54
|
-
const group = await sut.group([createMutant('src/index.ts', '', '')])
|
|
55
|
-
expect(group).toHaveLength(1)
|
|
56
|
-
// @ts-expect-error private close method
|
|
57
|
-
sut.tsCompiler.close()
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const fileContents: Record<string, string> = Object.freeze({
|
|
62
|
-
['src/index.ts']: fs.readFileSync(
|
|
63
|
-
resolveTestResource('src', 'index.ts'),
|
|
64
|
-
'utf8',
|
|
65
|
-
),
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
function createMutant(
|
|
69
|
-
fileName: 'src/index.ts',
|
|
70
|
-
findText: string,
|
|
71
|
-
replacement: string,
|
|
72
|
-
id = '42',
|
|
73
|
-
offset = 0,
|
|
74
|
-
): Mutant {
|
|
75
|
-
const lines = fileContents[fileName]!.split('\n')
|
|
76
|
-
const lineNumber = lines.findIndex((l) => l.includes(findText))
|
|
77
|
-
if (lineNumber === -1) {
|
|
78
|
-
throw new Error(`Cannot find ${findText} in ${fileName}`)
|
|
79
|
-
}
|
|
80
|
-
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
81
|
-
const location: Location = {
|
|
82
|
-
start: { line: lineNumber, column: textColumn + offset },
|
|
83
|
-
end: { line: lineNumber, column: textColumn + findText.length },
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
id,
|
|
87
|
-
fileName: resolveTestResource('src', fileName),
|
|
88
|
-
mutatorName: 'foo-mutator',
|
|
89
|
-
location,
|
|
90
|
-
replacement,
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
|
|
5
|
-
import { CheckStatus } from '@stryker-mutator/api/check'
|
|
6
|
-
import type { FailedCheckResult } from '@stryker-mutator/api/check'
|
|
7
|
-
import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
8
|
-
import type { Logger } from '@stryker-mutator/api/logging'
|
|
9
|
-
import { Either } from 'effect'
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
11
|
-
|
|
12
|
-
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
13
|
-
import { overrideOptions, parseTsConfig } from '../../src/tsconfig-helpers.js'
|
|
14
|
-
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
15
|
-
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
16
|
-
|
|
17
|
-
const resolveTestResource = path.resolve.bind(
|
|
18
|
-
path,
|
|
19
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
20
|
-
'..',
|
|
21
|
-
'..',
|
|
22
|
-
'testResources',
|
|
23
|
-
'single-project',
|
|
24
|
-
) as unknown as typeof path.resolve
|
|
25
|
-
|
|
26
|
-
function createLogger(): Logger {
|
|
27
|
-
return {
|
|
28
|
-
isTraceEnabled: () => false,
|
|
29
|
-
isDebugEnabled: () => false,
|
|
30
|
-
isInfoEnabled: () => false,
|
|
31
|
-
isWarnEnabled: () => false,
|
|
32
|
-
isErrorEnabled: () => false,
|
|
33
|
-
isFatalEnabled: () => false,
|
|
34
|
-
trace: () => {},
|
|
35
|
-
debug: () => {},
|
|
36
|
-
info: () => {},
|
|
37
|
-
warn: () => {},
|
|
38
|
-
error: () => {},
|
|
39
|
-
fatal: () => {},
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
44
|
-
const options = {
|
|
45
|
-
tsconfigFile,
|
|
46
|
-
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
47
|
-
} as unknown as StrykerOptions
|
|
48
|
-
const logger = createLogger()
|
|
49
|
-
const fileSystem = new HybridFileSystem()
|
|
50
|
-
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
51
|
-
return new TypescriptChecker(logger, options, compiler)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('Typescript checker on a single project', () => {
|
|
55
|
-
let sut: TypescriptChecker
|
|
56
|
-
|
|
57
|
-
beforeEach(() => {
|
|
58
|
-
sut = createChecker(resolveTestResource('tsconfig.json'))
|
|
59
|
-
return sut.init()
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
// @ts-expect-error private close method
|
|
64
|
-
sut.tsCompiler.close()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('should not write output to disk', () => {
|
|
68
|
-
expect(
|
|
69
|
-
fs.existsSync(resolveTestResource('dist')),
|
|
70
|
-
'Output was written to disk!',
|
|
71
|
-
).toBe(false)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should be able to validate a mutant that does not result in an error', async () => {
|
|
75
|
-
const mutant = createMutant(
|
|
76
|
-
'todo.ts',
|
|
77
|
-
'TodoList.allTodos.push(newItem)',
|
|
78
|
-
'newItem? 42: 43',
|
|
79
|
-
'42',
|
|
80
|
-
)
|
|
81
|
-
const actual = await sut.check([mutant])
|
|
82
|
-
expect(actual).toEqual({ '42': { status: CheckStatus.Passed } })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should be able invalidate a mutant that does result in a compile error', async () => {
|
|
86
|
-
const mutant = createMutant(
|
|
87
|
-
'todo.ts',
|
|
88
|
-
'TodoList.allTodos.push(newItem)',
|
|
89
|
-
'"This should not be a string 🙄"',
|
|
90
|
-
'mutId',
|
|
91
|
-
)
|
|
92
|
-
const actual = await sut.check([mutant])
|
|
93
|
-
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
94
|
-
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
95
|
-
'todo.ts(15,9): error TS2322',
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('should be able validate a mutant that does not result in a compile error after a compile error', async () => {
|
|
100
|
-
const mutantCompileError = createMutant(
|
|
101
|
-
'todo.ts',
|
|
102
|
-
'TodoList.allTodos.push(newItem)',
|
|
103
|
-
'"This should not be a string 🙄"',
|
|
104
|
-
)
|
|
105
|
-
const mutantWithoutError = createMutant(
|
|
106
|
-
'todo.ts',
|
|
107
|
-
'return TodoList.allTodos',
|
|
108
|
-
'[]',
|
|
109
|
-
'mut42',
|
|
110
|
-
7,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
await sut.check([mutantCompileError])
|
|
114
|
-
const actual = await sut.check([mutantWithoutError])
|
|
115
|
-
|
|
116
|
-
expect(actual).toEqual({ mut42: { status: CheckStatus.Passed } })
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('should be able to invalidate a mutant that results in an error in a different file', async () => {
|
|
120
|
-
const actual = await sut.check([
|
|
121
|
-
createMutant('todo.ts', 'return totalCount', '', '42'),
|
|
122
|
-
])
|
|
123
|
-
expect(actual['42']!.status).toBe(CheckStatus.CompileError)
|
|
124
|
-
expect((actual['42'] as FailedCheckResult).reason).toContain(
|
|
125
|
-
'todo.spec.ts(4,7): error TS2322',
|
|
126
|
-
)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('should be able to validate a mutant after a mutant in a different file resulted in a transpile error', async () => {
|
|
130
|
-
await sut.check([createMutant('todo.ts', 'return totalCount', '')])
|
|
131
|
-
const result = await sut.check([
|
|
132
|
-
createMutant(
|
|
133
|
-
'todo.spec.ts',
|
|
134
|
-
"'Mow lawn'",
|
|
135
|
-
"'this is valid, right?'",
|
|
136
|
-
'id42',
|
|
137
|
-
),
|
|
138
|
-
])
|
|
139
|
-
|
|
140
|
-
expect(result).toEqual({ id42: { status: CheckStatus.Passed } })
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('should be allow mutations in unrelated files', async () => {
|
|
144
|
-
const result = await sut.check([
|
|
145
|
-
createMutant('not-type-checked.js', 'bar', 'baz', 'id1'),
|
|
146
|
-
])
|
|
147
|
-
|
|
148
|
-
expect(result).toEqual({ id1: { status: CheckStatus.Passed } })
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('should allow unused local variables (override options)', async () => {
|
|
152
|
-
const mutant = createMutant(
|
|
153
|
-
'todo.ts',
|
|
154
|
-
'TodoList.allTodos.push(newItem)',
|
|
155
|
-
'42',
|
|
156
|
-
'id45',
|
|
157
|
-
)
|
|
158
|
-
const actual = await sut.check([mutant])
|
|
159
|
-
expect(actual).toEqual({ id45: { status: CheckStatus.Passed } })
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('should be able invalidate 2 mutants that do result in a compile errors', async () => {
|
|
163
|
-
const mutant = createMutant(
|
|
164
|
-
'todo.ts',
|
|
165
|
-
'TodoList.allTodos.push(newItem)',
|
|
166
|
-
'"This should not be a string 🙄"',
|
|
167
|
-
'mutId',
|
|
168
|
-
)
|
|
169
|
-
const mutant2 = createMutant(
|
|
170
|
-
'counter.ts',
|
|
171
|
-
'return this.currentNumber',
|
|
172
|
-
'return "This should not return a string 🙄"',
|
|
173
|
-
'mutId2',
|
|
174
|
-
)
|
|
175
|
-
const actual = await sut.check([mutant, mutant2])
|
|
176
|
-
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
177
|
-
expect(actual['mutId2']!.status).toBe(CheckStatus.CompileError)
|
|
178
|
-
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
179
|
-
'todo.ts(15,9): error TS2322',
|
|
180
|
-
)
|
|
181
|
-
expect((actual['mutId2'] as FailedCheckResult).reason).toContain(
|
|
182
|
-
'counter.ts(7,5): error TS2322',
|
|
183
|
-
)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('should be able invalidate 2 mutants that do result in a compile error in file above', async () => {
|
|
187
|
-
const mutant = createMutant(
|
|
188
|
-
'errorInFileAbove2Mutants/todo.ts',
|
|
189
|
-
'TodoList.allTodos.push(newItem)',
|
|
190
|
-
'"This should not be a string 🙄"',
|
|
191
|
-
'mutId',
|
|
192
|
-
)
|
|
193
|
-
const mutant2 = createMutant(
|
|
194
|
-
'errorInFileAbove2Mutants/counter.ts',
|
|
195
|
-
'return (this.currentNumber += numberToIncrementBy)',
|
|
196
|
-
'return "This should not return a string 🙄"',
|
|
197
|
-
'mutId2',
|
|
198
|
-
)
|
|
199
|
-
const actual = await sut.check([mutant, mutant2])
|
|
200
|
-
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
201
|
-
expect(actual['mutId2']!.status).toBe(CheckStatus.CompileError)
|
|
202
|
-
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
203
|
-
'todo.ts(15,9): error TS2322',
|
|
204
|
-
)
|
|
205
|
-
expect((actual['mutId2'] as FailedCheckResult).reason).toContain(
|
|
206
|
-
'errorInFileAbove2Mutants/todo-counter.ts(7,7): error TS2322',
|
|
207
|
-
)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('should compile the fixture source files discovered via the ** include glob', async () => {
|
|
211
|
-
// The fixture tsconfig selects its inputs via `include: ["src/**/*.ts"]`. If a comment
|
|
212
|
-
// stripper corrupted the glob (the `/**/` inside it looks like an empty block comment),
|
|
213
|
-
// TypeScript would find no input files and abort init with TS18003, so this assertion
|
|
214
|
-
// pins the end-to-end regression against the real checker path.
|
|
215
|
-
const mutant = createMutant(
|
|
216
|
-
'errorInFileAbove2Mutants/todo.ts',
|
|
217
|
-
'TodoList.allTodos.push(newItem)',
|
|
218
|
-
'"This should not be a string 🙄"',
|
|
219
|
-
'glob-pin',
|
|
220
|
-
)
|
|
221
|
-
const actual = await sut.check([mutant])
|
|
222
|
-
expect(actual['glob-pin']?.status).toBe(CheckStatus.CompileError)
|
|
223
|
-
expect((actual['glob-pin'] as FailedCheckResult).reason).toContain(
|
|
224
|
-
'todo.ts(15,9): error TS2322',
|
|
225
|
-
)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('should preserve the fixture $schema URL and ** glob in the config produced by overrideOptions', () => {
|
|
229
|
-
// `$schema` holds a `//`-bearing string and `include` holds a `**` glob: the two shapes a
|
|
230
|
-
// naive comment stripper mangles. The compiler feeds the output of `overrideOptions` to
|
|
231
|
-
// TypeScript, so this asserts both survive byte-for-byte into that config.
|
|
232
|
-
const tsconfigFile = resolveTestResource('tsconfig.json')
|
|
233
|
-
const content = fs.readFileSync(tsconfigFile, 'utf8')
|
|
234
|
-
const parsed = parseTsConfig(tsconfigFile, content)
|
|
235
|
-
if (Either.isLeft(parsed)) {
|
|
236
|
-
throw new Error(`Expected fixture tsconfig to parse, got: ${parsed.left.reason}`)
|
|
237
|
-
}
|
|
238
|
-
const config = overrideOptions(parsed.right, false)
|
|
239
|
-
expect(config).toContain('"$schema":"https://json.schemastore.org/tsconfig"')
|
|
240
|
-
expect(config).toContain('"include":["src/**/*.ts"]')
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
const fileContents: Record<string, string> = Object.freeze({
|
|
245
|
-
['errorInFileAbove2Mutants/todo.ts']: fs.readFileSync(
|
|
246
|
-
resolveTestResource('src', 'errorInFileAbove2Mutants', 'todo.ts'),
|
|
247
|
-
'utf8',
|
|
248
|
-
),
|
|
249
|
-
['errorInFileAbove2Mutants/counter.ts']: fs.readFileSync(
|
|
250
|
-
resolveTestResource('src', 'errorInFileAbove2Mutants', 'counter.ts'),
|
|
251
|
-
'utf8',
|
|
252
|
-
),
|
|
253
|
-
['todo.ts']: fs.readFileSync(resolveTestResource('src', 'todo.ts'), 'utf8'),
|
|
254
|
-
['counter.ts']: fs.readFileSync(
|
|
255
|
-
resolveTestResource('src', 'counter.ts'),
|
|
256
|
-
'utf8',
|
|
257
|
-
),
|
|
258
|
-
['todo.spec.ts']: fs.readFileSync(
|
|
259
|
-
resolveTestResource('src', 'todo.spec.ts'),
|
|
260
|
-
'utf8',
|
|
261
|
-
),
|
|
262
|
-
['not-type-checked.js']: fs.readFileSync(
|
|
263
|
-
resolveTestResource('src', 'not-type-checked.js'),
|
|
264
|
-
'utf8',
|
|
265
|
-
),
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
function createMutant(
|
|
269
|
-
fileName:
|
|
270
|
-
| 'counter.ts'
|
|
271
|
-
| 'errorInFileAbove2Mutants/counter.ts'
|
|
272
|
-
| 'errorInFileAbove2Mutants/todo.ts'
|
|
273
|
-
| 'not-type-checked.js'
|
|
274
|
-
| 'todo.spec.ts'
|
|
275
|
-
| 'todo.ts',
|
|
276
|
-
findText: string,
|
|
277
|
-
replacement: string,
|
|
278
|
-
id = '42',
|
|
279
|
-
offset = 0,
|
|
280
|
-
): Mutant {
|
|
281
|
-
const lines = fileContents[fileName]!.split('\n')
|
|
282
|
-
const lineNumber = lines.findIndex((line) => line.includes(findText))
|
|
283
|
-
if (lineNumber === -1) {
|
|
284
|
-
throw new Error(`Cannot find ${findText} in ${fileName}`)
|
|
285
|
-
}
|
|
286
|
-
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
287
|
-
const location: Location = {
|
|
288
|
-
start: { line: lineNumber, column: textColumn + offset },
|
|
289
|
-
end: { line: lineNumber, column: textColumn + findText.length },
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
id,
|
|
293
|
-
fileName: resolveTestResource('src', fileName),
|
|
294
|
-
mutatorName: 'foo-mutator',
|
|
295
|
-
location,
|
|
296
|
-
replacement,
|
|
297
|
-
}
|
|
298
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
-
import { tmpdir } from 'os'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
import { Either } from 'effect'
|
|
6
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
determineBuildModeEnabled,
|
|
10
|
-
overrideOptions,
|
|
11
|
-
parseTsConfig,
|
|
12
|
-
TsConfigParseError,
|
|
13
|
-
} from '../../src/tsconfig-helpers.js'
|
|
14
|
-
|
|
15
|
-
function expectRight<A, E>(either: Either.Either<A, E>): A {
|
|
16
|
-
if (Either.isLeft(either)) {
|
|
17
|
-
throw new Error(`Expected a Right result, got a Left: ${String(either.left)}`)
|
|
18
|
-
}
|
|
19
|
-
return either.right
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('parseTsConfig', () => {
|
|
23
|
-
it('should preserve a glob pattern in include exactly', () => {
|
|
24
|
-
const config = expectRight(
|
|
25
|
-
parseTsConfig('tsconfig.json', '{"include":["src/**/*.workflow.ts"]}'),
|
|
26
|
-
)
|
|
27
|
-
expect(config).toEqual({ include: ['src/**/*.workflow.ts'] })
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should round-trip a $schema URL unchanged', () => {
|
|
31
|
-
const config = expectRight(
|
|
32
|
-
parseTsConfig('tsconfig.json', '{"$schema":"https://json.schemastore.org/tsconfig"}'),
|
|
33
|
-
)
|
|
34
|
-
expect(config).toEqual({
|
|
35
|
-
$schema: 'https://json.schemastore.org/tsconfig',
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should round-trip a Windows path and an escaped quote unchanged', () => {
|
|
40
|
-
const config = expectRight(
|
|
41
|
-
parseTsConfig(
|
|
42
|
-
'tsconfig.json',
|
|
43
|
-
'{"paths":{"a":["C:\\\\x//y"]},"description":"he said \\"hi\\""}',
|
|
44
|
-
),
|
|
45
|
-
)
|
|
46
|
-
expect(config).toEqual({
|
|
47
|
-
paths: { a: ['C:\\x//y'] },
|
|
48
|
-
description: 'he said "hi"',
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should accept trailing commas in nested objects and arrays', () => {
|
|
53
|
-
const config = expectRight(
|
|
54
|
-
parseTsConfig(
|
|
55
|
-
'tsconfig.json',
|
|
56
|
-
'{"compilerOptions":{"strict":true,},"include":["a.ts","b.ts",]}',
|
|
57
|
-
),
|
|
58
|
-
)
|
|
59
|
-
expect(config).toEqual({
|
|
60
|
-
compilerOptions: { strict: true },
|
|
61
|
-
include: ['a.ts', 'b.ts'],
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('should accept content prefixed with a BOM', () => {
|
|
66
|
-
const withoutBom = expectRight(
|
|
67
|
-
parseTsConfig('tsconfig.json', '{"compilerOptions":{"strict":true}}'),
|
|
68
|
-
)
|
|
69
|
-
const withBom = expectRight(
|
|
70
|
-
parseTsConfig('tsconfig.json', '\uFEFF{"compilerOptions":{"strict":true}}'),
|
|
71
|
-
)
|
|
72
|
-
expect(withBom).toEqual(withoutBom)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('should strip line and block comments', () => {
|
|
76
|
-
const config = expectRight(
|
|
77
|
-
parseTsConfig(
|
|
78
|
-
'tsconfig.json',
|
|
79
|
-
'{\n// a line comment\n"a": 1,\n/* a block comment */\n"b": 2\n}',
|
|
80
|
-
),
|
|
81
|
-
)
|
|
82
|
-
expect(config).toEqual({ a: 1, b: 2 })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should preserve unknown nested keys inside compilerOptions', () => {
|
|
86
|
-
const config = expectRight(
|
|
87
|
-
parseTsConfig(
|
|
88
|
-
'tsconfig.json',
|
|
89
|
-
'{"compilerOptions":{"strict":true,"experimentalDecorators":true}}',
|
|
90
|
-
),
|
|
91
|
-
)
|
|
92
|
-
expect(config.compilerOptions).toEqual({
|
|
93
|
-
strict: true,
|
|
94
|
-
experimentalDecorators: true,
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should return a Left for a malformed document', () => {
|
|
99
|
-
const result = parseTsConfig('tsconfig.json', '{ "a": }')
|
|
100
|
-
expect(Either.isLeft(result)).toBe(true)
|
|
101
|
-
if (Either.isLeft(result)) {
|
|
102
|
-
expect(result.left).toBeInstanceOf(TsConfigParseError)
|
|
103
|
-
expect(result.left.file).toBe('tsconfig.json')
|
|
104
|
-
expect(result.left.reason.length).toBeGreaterThan(0)
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('should return a Left for truncated input', () => {
|
|
109
|
-
for (const truncated of ['{', '{"a":']) {
|
|
110
|
-
expect(Either.isLeft(parseTsConfig('tsconfig.json', truncated))).toBe(true)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('should return a Left for an unterminated block comment', () => {
|
|
115
|
-
const result = parseTsConfig('tsconfig.json', '{"a":1,/* never closed')
|
|
116
|
-
expect(Either.isLeft(result)).toBe(true)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('should return a Left for a bare string root (not a config object)', () => {
|
|
120
|
-
const result = parseTsConfig('tsconfig.json', '"just a string"')
|
|
121
|
-
expect(Either.isLeft(result)).toBe(true)
|
|
122
|
-
if (Either.isLeft(result)) {
|
|
123
|
-
expect(result.left).toBeInstanceOf(TsConfigParseError)
|
|
124
|
-
expect(result.left.file).toBe('tsconfig.json')
|
|
125
|
-
expect(result.left.reason.length).toBeGreaterThan(0)
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('should return a Left when references is not an array', () => {
|
|
130
|
-
const result = parseTsConfig('tsconfig.json', '{"references":"nope"}')
|
|
131
|
-
expect(Either.isLeft(result)).toBe(true)
|
|
132
|
-
if (Either.isLeft(result)) {
|
|
133
|
-
expect(result.left).toBeInstanceOf(TsConfigParseError)
|
|
134
|
-
expect(result.left.reason.length).toBeGreaterThan(0)
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
describe('overrideOptions', () => {
|
|
140
|
-
it('should apply the compiler-option overrides to an empty config', () => {
|
|
141
|
-
const parsed = JSON.parse(overrideOptions({}, false))
|
|
142
|
-
expect(parsed.compilerOptions).toMatchObject({
|
|
143
|
-
allowUnreachableCode: true,
|
|
144
|
-
noEmit: true,
|
|
145
|
-
target: 'es2022',
|
|
146
|
-
moduleResolution: 'bundler',
|
|
147
|
-
})
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('should preserve unknown top-level keys and unknown compilerOptions through to the output', () => {
|
|
151
|
-
const config = expectRight(
|
|
152
|
-
parseTsConfig(
|
|
153
|
-
'tsconfig.json',
|
|
154
|
-
'{"include":["src/**/*.ts"],"compilerOptions":{"strict":true,"experimentalDecorators":true}}',
|
|
155
|
-
),
|
|
156
|
-
)
|
|
157
|
-
const output = JSON.parse(overrideOptions(config, false))
|
|
158
|
-
expect(output.include).toEqual(['src/**/*.ts'])
|
|
159
|
-
expect(output.compilerOptions.strict).toBe(true)
|
|
160
|
-
expect(output.compilerOptions.experimentalDecorators).toBe(true)
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('determineBuildModeEnabled', () => {
|
|
165
|
-
let tempDir: string | undefined
|
|
166
|
-
|
|
167
|
-
afterEach(() => {
|
|
168
|
-
if (tempDir !== undefined) {
|
|
169
|
-
rmSync(tempDir, { recursive: true, force: true })
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
function createTsconfig(content: string): string {
|
|
174
|
-
tempDir = mkdtempSync(path.join(tmpdir(), 'tsconfig-helpers-'))
|
|
175
|
-
const tsconfigPath = path.join(tempDir, 'tsconfig.json')
|
|
176
|
-
writeFileSync(tsconfigPath, content)
|
|
177
|
-
return tsconfigPath
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
it('should return true when the config has references', () => {
|
|
181
|
-
const tsconfigPath = createTsconfig('{"references":[{"path":"./a"}]}')
|
|
182
|
-
expect(determineBuildModeEnabled(tsconfigPath)).toBe(true)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('should return false when the config has no references', () => {
|
|
186
|
-
const tsconfigPath = createTsconfig('{"compilerOptions":{"strict":true}}')
|
|
187
|
-
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('should return false when the config is a bare string instead of throwing a TypeError', () => {
|
|
191
|
-
const tsconfigPath = createTsconfig('"just a string"')
|
|
192
|
-
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('should return false when references is not an array', () => {
|
|
196
|
-
const tsconfigPath = createTsconfig('{"references":"nope"}')
|
|
197
|
-
expect(determineBuildModeEnabled(tsconfigPath)).toBe(false)
|
|
198
|
-
})
|
|
199
|
-
})
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import { fileURLToPath } from 'url'
|
|
3
|
-
|
|
4
|
-
import type { StrykerOptions } from '@stryker-mutator/api/core'
|
|
5
|
-
import type { Logger } from '@stryker-mutator/api/logging'
|
|
6
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
-
|
|
8
|
-
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
9
|
-
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
10
|
-
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
11
|
-
|
|
12
|
-
const resolveTestResource = path.resolve.bind(
|
|
13
|
-
path,
|
|
14
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
15
|
-
'..',
|
|
16
|
-
'..',
|
|
17
|
-
'testResources',
|
|
18
|
-
'errors',
|
|
19
|
-
) as unknown as typeof path.resolve
|
|
20
|
-
|
|
21
|
-
function createLogger(warn: Logger['warn'] = () => {}): Logger {
|
|
22
|
-
return {
|
|
23
|
-
isTraceEnabled: () => false,
|
|
24
|
-
isDebugEnabled: () => false,
|
|
25
|
-
isInfoEnabled: () => false,
|
|
26
|
-
isWarnEnabled: () => false,
|
|
27
|
-
isErrorEnabled: () => false,
|
|
28
|
-
isFatalEnabled: () => false,
|
|
29
|
-
trace: () => {},
|
|
30
|
-
debug: () => {},
|
|
31
|
-
info: () => {},
|
|
32
|
-
warn,
|
|
33
|
-
error: () => {},
|
|
34
|
-
fatal: () => {},
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createChecker(
|
|
39
|
-
tsconfigFile: string,
|
|
40
|
-
logger: Logger = createLogger(),
|
|
41
|
-
): TypescriptChecker {
|
|
42
|
-
const options = {
|
|
43
|
-
tsconfigFile,
|
|
44
|
-
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
45
|
-
} as unknown as StrykerOptions
|
|
46
|
-
const fileSystem = new HybridFileSystem()
|
|
47
|
-
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
48
|
-
return new TypescriptChecker(logger, options, compiler)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('Typescript checker errors', () => {
|
|
52
|
-
it('should reject initialization if initial compilation failed', async () => {
|
|
53
|
-
const sut = createChecker(
|
|
54
|
-
resolveTestResource('compile-error', 'tsconfig.json'),
|
|
55
|
-
)
|
|
56
|
-
await expect(sut.init()).rejects.toThrow(
|
|
57
|
-
'Typescript error(s) found in dry run compilation:',
|
|
58
|
-
)
|
|
59
|
-
await expect(sut.init()).rejects.toThrow(
|
|
60
|
-
'testResources/errors/compile-error/add.ts(2,3): error TS2322:',
|
|
61
|
-
)
|
|
62
|
-
}, 30_000)
|
|
63
|
-
|
|
64
|
-
it('should reject initialization if tsconfig was invalid', async () => {
|
|
65
|
-
const sut = createChecker(
|
|
66
|
-
resolveTestResource('invalid-tsconfig', 'tsconfig.json'),
|
|
67
|
-
)
|
|
68
|
-
await expect(sut.init()).rejects.toThrow(
|
|
69
|
-
'Typescript error(s) found in dry run compilation:',
|
|
70
|
-
)
|
|
71
|
-
await expect(sut.init()).rejects.toThrow(
|
|
72
|
-
'testResources/errors/invalid-tsconfig/tsconfig.json(1,1): error TS1005:',
|
|
73
|
-
)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should log a warning naming the tsconfig path and the skipped-overrides consequence when parsing falls back', async () => {
|
|
77
|
-
const warn = vi.fn()
|
|
78
|
-
const sut = createChecker(
|
|
79
|
-
resolveTestResource('invalid-tsconfig', 'tsconfig.json'),
|
|
80
|
-
createLogger(warn),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
await expect(sut.init()).rejects.toThrow(
|
|
84
|
-
'testResources/errors/invalid-tsconfig/tsconfig.json(1,1): error TS1005:',
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
expect(warn).toHaveBeenCalled()
|
|
88
|
-
const warnings = warn.mock.calls
|
|
89
|
-
.map((call) => call.join(' '))
|
|
90
|
-
.join('\n')
|
|
91
|
-
expect(warnings).toContain(
|
|
92
|
-
'testResources/errors/invalid-tsconfig/tsconfig.json',
|
|
93
|
-
)
|
|
94
|
-
expect(warnings.toLowerCase()).toContain(
|
|
95
|
-
'compiler-option overrides and project-reference walking were skipped',
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it("should reject when tsconfig file doesn't exist", async () => {
|
|
100
|
-
const sut = createChecker(
|
|
101
|
-
resolveTestResource('empty-dir', 'tsconfig.json'),
|
|
102
|
-
)
|
|
103
|
-
await expect(sut.init()).rejects.toThrow(
|
|
104
|
-
`The tsconfig file does not exist at: "${
|
|
105
|
-
resolveTestResource(
|
|
106
|
-
'empty-dir',
|
|
107
|
-
'tsconfig.json',
|
|
108
|
-
)
|
|
109
|
-
}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`,
|
|
110
|
-
)
|
|
111
|
-
})
|
|
112
|
-
})
|