@systemfsoftware/stryker-js-typescript-checker 1.2.3 → 1.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/CHANGELOG.md +65 -0
- package/LICENSE +203 -21
- package/README.md +31 -0
- package/dist/index.d.mts +19 -19
- package/dist/index.mjs +132 -120
- package/package.json +24 -11
- 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/nodenext-project.it.spec.ts +0 -101
- 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 -218
- 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 -5
- package/testResources/errors/empty-dir/.gitkeep +0 -0
- package/testResources/errors/invalid-tsconfig/tsconfig.json +0 -1
- package/testResources/nodenext-project/package.json +0 -6
- package/testResources/nodenext-project/src/index.ts +0 -5
- package/testResources/nodenext-project/src/util.ts +0 -3
- package/testResources/nodenext-project/tsconfig.json +0 -16
- 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 -15
- 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 -15
- 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 -15
- package/tsconfig.json +0 -26
- package/tsdown.config.ts +0 -16
- package/vitest.config.ts +0 -13
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
|
|
4
|
-
import type { Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
5
|
-
import type { Logger } from '@stryker-mutator/api/logging'
|
|
6
|
-
import { commonTokens, tokens } from '@stryker-mutator/api/plugin'
|
|
7
|
-
import { Either } from 'effect'
|
|
8
|
-
import { type SourceFile, SyntaxKind } from 'typescript/unstable/ast'
|
|
9
|
-
import type { FileSystem } from 'typescript/unstable/fs'
|
|
10
|
-
import { API, type Diagnostic, type DocumentIdentifier, type Program, type Snapshot } from 'typescript/unstable/sync'
|
|
11
|
-
|
|
12
|
-
import { HybridFileSystem } from './fs/index.js'
|
|
13
|
-
import { TSFileNode } from './grouping/ts-file-node.js'
|
|
14
|
-
import * as pluginTokens from './plugin-tokens.js'
|
|
15
|
-
import {
|
|
16
|
-
determineBuildModeEnabled,
|
|
17
|
-
getSourceMappingURL,
|
|
18
|
-
guardTSVersion,
|
|
19
|
-
overrideOptions,
|
|
20
|
-
parseTsConfig,
|
|
21
|
-
retrieveReferencedProjects,
|
|
22
|
-
toPosixFileName,
|
|
23
|
-
} from './tsconfig-helpers.js'
|
|
24
|
-
|
|
25
|
-
export interface ITypescriptCompiler {
|
|
26
|
-
init(): Promise<Diagnostic[]>
|
|
27
|
-
check(mutants: Mutant[]): Promise<Diagnostic[]>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface IFileRelationCreator {
|
|
31
|
-
get nodes(): Map<string, TSFileNode>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type SourceFiles = Map<
|
|
35
|
-
string,
|
|
36
|
-
{
|
|
37
|
-
fileName: string
|
|
38
|
-
imports: Set<string>
|
|
39
|
-
}
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
export class TypescriptCompiler implements ITypescriptCompiler, IFileRelationCreator {
|
|
43
|
-
public static inject = tokens(
|
|
44
|
-
commonTokens.logger,
|
|
45
|
-
commonTokens.options,
|
|
46
|
-
pluginTokens.fs,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
private readonly allTSConfigFiles: Set<string>
|
|
50
|
-
private readonly tsconfigFile: string
|
|
51
|
-
private api?: API
|
|
52
|
-
private snapshot?: Snapshot
|
|
53
|
-
private readonly sourceFiles: SourceFiles = new Map()
|
|
54
|
-
private readonly _nodes = new Map<string, TSFileNode>()
|
|
55
|
-
private lastMutants: Mutant[] = []
|
|
56
|
-
private lastMutatedFileNames: string[] = []
|
|
57
|
-
|
|
58
|
-
constructor(
|
|
59
|
-
private readonly log: Logger,
|
|
60
|
-
private readonly options: StrykerOptions,
|
|
61
|
-
private readonly fs: HybridFileSystem,
|
|
62
|
-
) {
|
|
63
|
-
this.tsconfigFile = toPosixFileName(
|
|
64
|
-
path.resolve(toPosixFileName(this.options.tsconfigFile)),
|
|
65
|
-
)
|
|
66
|
-
this.allTSConfigFiles = new Set<string>([this.tsconfigFile])
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public async init(): Promise<Diagnostic[]> {
|
|
70
|
-
guardTSVersion()
|
|
71
|
-
this.guardTSConfigFileExists()
|
|
72
|
-
const buildModeEnabled = determineBuildModeEnabled(this.tsconfigFile)
|
|
73
|
-
|
|
74
|
-
this.collectAllTSConfigFiles(buildModeEnabled)
|
|
75
|
-
this.api = new API({ fs: this.fs as FileSystem })
|
|
76
|
-
this.snapshot = this.api.updateSnapshot({
|
|
77
|
-
openProjects: [...this.allTSConfigFiles],
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const programs = this.getPrograms()
|
|
81
|
-
this.buildDependencyGraph(programs)
|
|
82
|
-
|
|
83
|
-
return this.check([])
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public async check(mutants: Mutant[]): Promise<Diagnostic[]> {
|
|
87
|
-
// Reset previous mutations
|
|
88
|
-
for (const mutant of this.lastMutants) {
|
|
89
|
-
this.fs.resetFile(mutant.fileName)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Apply new mutations
|
|
93
|
-
for (const mutant of mutants) {
|
|
94
|
-
const file = this.fs.getFile(mutant.fileName)
|
|
95
|
-
if (!file) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`Tried to check file "${mutant.fileName}" (which is part of your typescript project), but it could not be found.`,
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
file.mutate(mutant)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const mutatedFileNames = [
|
|
104
|
-
...new Set(mutants.map((m) => toPosixFileName(m.fileName))),
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
const changedFiles = [
|
|
108
|
-
...new Set([...this.lastMutatedFileNames, ...mutatedFileNames]),
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
if (this.api && this.snapshot) {
|
|
112
|
-
const oldSnapshot = this.snapshot
|
|
113
|
-
this.snapshot = this.api.updateSnapshot({
|
|
114
|
-
openProjects: [...this.allTSConfigFiles],
|
|
115
|
-
fileChanges: { changed: changedFiles },
|
|
116
|
-
})
|
|
117
|
-
oldSnapshot.dispose()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
this.lastMutants = mutants
|
|
121
|
-
this.lastMutatedFileNames = mutatedFileNames
|
|
122
|
-
|
|
123
|
-
const programs = this.getPrograms()
|
|
124
|
-
return programs.flatMap((program) => [
|
|
125
|
-
...program.getConfigFileParsingDiagnostics(),
|
|
126
|
-
...program.getSemanticDiagnostics(),
|
|
127
|
-
...program.getProgramDiagnostics(),
|
|
128
|
-
])
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public get nodes(): Map<string, TSFileNode> {
|
|
132
|
-
if (!this._nodes.size) {
|
|
133
|
-
// create nodes
|
|
134
|
-
for (const [fileName] of this.sourceFiles) {
|
|
135
|
-
const node = new TSFileNode(fileName, [], [])
|
|
136
|
-
this._nodes.set(fileName, node)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// set children
|
|
140
|
-
for (const [fileName, file] of this.sourceFiles) {
|
|
141
|
-
const node = this._nodes.get(fileName)
|
|
142
|
-
if (node == null) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`Node for file '${fileName}' could not be found. This should not happen.`,
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
node.children = [...file.imports]
|
|
149
|
-
.map((importName) => this._nodes.get(importName))
|
|
150
|
-
.filter((n): n is TSFileNode => n != null)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// set parents
|
|
154
|
-
for (const [, node] of this._nodes) {
|
|
155
|
-
node.parents = []
|
|
156
|
-
for (const [, n] of this._nodes) {
|
|
157
|
-
if (n.children.includes(node)) {
|
|
158
|
-
node.parents.push(n)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return this._nodes
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
public close(): void {
|
|
168
|
-
this.snapshot?.dispose()
|
|
169
|
-
this.api?.close()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
public getLineAndCharacterOfPosition(
|
|
173
|
-
fileName: string,
|
|
174
|
-
position: number,
|
|
175
|
-
): { line: number; character: number } | undefined {
|
|
176
|
-
for (const program of this.getPrograms()) {
|
|
177
|
-
const sourceFile = program.getSourceFile(fileName as DocumentIdentifier)
|
|
178
|
-
if (sourceFile) {
|
|
179
|
-
return sourceFile.getLineAndCharacterOfPosition(position)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return undefined
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private getPrograms(): Program[] {
|
|
186
|
-
if (!this.snapshot) {
|
|
187
|
-
throw new Error('TypescriptCompiler not initialized')
|
|
188
|
-
}
|
|
189
|
-
const projects = this.snapshot.getProjects()
|
|
190
|
-
if (projects.length === 0) {
|
|
191
|
-
throw new Error(`No projects found for ${this.tsconfigFile}`)
|
|
192
|
-
}
|
|
193
|
-
return projects.map((project) => project.program)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private collectAllTSConfigFiles(buildModeEnabled: boolean): void {
|
|
197
|
-
const tsConfigOverrides = new Map<string, string>()
|
|
198
|
-
const toProcess = [this.tsconfigFile]
|
|
199
|
-
const processed = new Set<string>()
|
|
200
|
-
|
|
201
|
-
while (toProcess.length > 0) {
|
|
202
|
-
const current = toProcess.pop()
|
|
203
|
-
if (!current || processed.has(current)) {
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
206
|
-
processed.add(current)
|
|
207
|
-
|
|
208
|
-
const content = readFileSync(current, 'utf-8')
|
|
209
|
-
const parsed = parseTsConfig(current, content)
|
|
210
|
-
if (Either.isLeft(parsed)) {
|
|
211
|
-
this.log.warn(
|
|
212
|
-
`Could not parse tsconfig file "%s": %s. Compiler-option overrides and project-reference walking were skipped for this file, so mutants may be misreported as compile errors.`,
|
|
213
|
-
current,
|
|
214
|
-
parsed.left.reason,
|
|
215
|
-
)
|
|
216
|
-
tsConfigOverrides.set(current, content)
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
tsConfigOverrides.set(current, overrideOptions(parsed.right, buildModeEnabled))
|
|
220
|
-
|
|
221
|
-
for (
|
|
222
|
-
const referenced of retrieveReferencedProjects(
|
|
223
|
-
parsed.right,
|
|
224
|
-
path.dirname(current),
|
|
225
|
-
)
|
|
226
|
-
) {
|
|
227
|
-
this.allTSConfigFiles.add(referenced)
|
|
228
|
-
toProcess.push(referenced)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
this.fs.tsConfigOverrides = tsConfigOverrides
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private buildDependencyGraph(programs: Program[]): void {
|
|
236
|
-
for (const program of programs) {
|
|
237
|
-
for (const fileName of program.getSourceFileNames()) {
|
|
238
|
-
if (
|
|
239
|
-
fileName.endsWith('.d.ts') ||
|
|
240
|
-
fileName.includes('node_modules')
|
|
241
|
-
) {
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
244
|
-
const normalized = toPosixFileName(fileName)
|
|
245
|
-
this.sourceFiles.set(normalized, {
|
|
246
|
-
fileName: normalized,
|
|
247
|
-
imports: new Set(),
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
for (const [fileName] of this.sourceFiles) {
|
|
253
|
-
const sourceFile = programs
|
|
254
|
-
.map((p) => p.getSourceFile(fileName as DocumentIdentifier))
|
|
255
|
-
.find((sf) => sf != null)
|
|
256
|
-
if (!sourceFile) {
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
const imports = this.extractImports(sourceFile)
|
|
260
|
-
for (const specifier of imports) {
|
|
261
|
-
const resolved = this.resolveModuleSpecifier(fileName, specifier)
|
|
262
|
-
if (resolved) {
|
|
263
|
-
const sourceFileName = this.resolveTSInputFile(resolved)
|
|
264
|
-
if (this.sourceFiles.has(sourceFileName)) {
|
|
265
|
-
this.sourceFiles.get(fileName)?.imports.add(sourceFileName)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private extractImports(sourceFile: SourceFile): string[] {
|
|
273
|
-
const result: string[] = []
|
|
274
|
-
|
|
275
|
-
for (const statement of sourceFile.statements) {
|
|
276
|
-
if (statement.kind === SyntaxKind.ImportDeclaration) {
|
|
277
|
-
let spec: SourceFile['imports'][number] | undefined
|
|
278
|
-
statement.forEachChild((child) => {
|
|
279
|
-
if (child.kind === SyntaxKind.StringLiteral) {
|
|
280
|
-
spec = child
|
|
281
|
-
}
|
|
282
|
-
})
|
|
283
|
-
if (spec) {
|
|
284
|
-
result.push(spec.getText(sourceFile))
|
|
285
|
-
}
|
|
286
|
-
} else if (statement.kind === SyntaxKind.ImportEqualsDeclaration) {
|
|
287
|
-
statement.forEachChild((child) => {
|
|
288
|
-
if (child.kind === SyntaxKind.ExternalModuleReference) {
|
|
289
|
-
child.forEachChild((refChild) => {
|
|
290
|
-
if (refChild.kind === SyntaxKind.StringLiteral) {
|
|
291
|
-
result.push(refChild.getText(sourceFile))
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
for (const ref of sourceFile.referencedFiles) {
|
|
300
|
-
result.push(ref.fileName)
|
|
301
|
-
}
|
|
302
|
-
for (const ref of sourceFile.typeReferenceDirectives) {
|
|
303
|
-
result.push(ref.fileName)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return result
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private resolveModuleSpecifier(
|
|
310
|
-
sourceFileName: string,
|
|
311
|
-
specifier: string,
|
|
312
|
-
): string | undefined {
|
|
313
|
-
const cleaned = specifier.replace(/^['"]|['"]$/g, '')
|
|
314
|
-
if (!cleaned.startsWith('./') && !cleaned.startsWith('../')) {
|
|
315
|
-
return undefined
|
|
316
|
-
}
|
|
317
|
-
const baseDir = path.dirname(sourceFileName)
|
|
318
|
-
const resolved = toPosixFileName(path.resolve(baseDir, cleaned))
|
|
319
|
-
|
|
320
|
-
const candidates = this.getResolutionCandidates(resolved)
|
|
321
|
-
for (const candidate of candidates) {
|
|
322
|
-
if (this.sourceFiles.has(candidate)) {
|
|
323
|
-
return candidate
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return undefined
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private getResolutionCandidates(resolved: string): string[] {
|
|
331
|
-
const extension = path.extname(resolved)
|
|
332
|
-
if (extension) {
|
|
333
|
-
const withoutExt = resolved.slice(0, -extension.length)
|
|
334
|
-
return [
|
|
335
|
-
resolved,
|
|
336
|
-
`${withoutExt}.ts`,
|
|
337
|
-
`${withoutExt}.tsx`,
|
|
338
|
-
`${withoutExt}.d.ts`,
|
|
339
|
-
`${withoutExt}.js`,
|
|
340
|
-
`${withoutExt}.jsx`,
|
|
341
|
-
`${withoutExt}.mjs`,
|
|
342
|
-
`${withoutExt}.cjs`,
|
|
343
|
-
]
|
|
344
|
-
}
|
|
345
|
-
return [
|
|
346
|
-
resolved,
|
|
347
|
-
`${resolved}.ts`,
|
|
348
|
-
`${resolved}.tsx`,
|
|
349
|
-
`${resolved}.d.ts`,
|
|
350
|
-
`${resolved}/index.ts`,
|
|
351
|
-
`${resolved}/index.tsx`,
|
|
352
|
-
`${resolved}/index.d.ts`,
|
|
353
|
-
`${resolved}.js`,
|
|
354
|
-
`${resolved}.jsx`,
|
|
355
|
-
`${resolved}.mjs`,
|
|
356
|
-
`${resolved}.cjs`,
|
|
357
|
-
`${resolved}/index.js`,
|
|
358
|
-
`${resolved}/index.jsx`,
|
|
359
|
-
`${resolved}/index.mjs`,
|
|
360
|
-
`${resolved}/index.cjs`,
|
|
361
|
-
]
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private resolveTSInputFile(dependencyFileName: string): string {
|
|
365
|
-
if (!dependencyFileName.endsWith('.d.ts')) {
|
|
366
|
-
return dependencyFileName
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const file = this.fs.getFile(dependencyFileName)
|
|
370
|
-
if (!file) {
|
|
371
|
-
return dependencyFileName
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const sourceMappingURL = getSourceMappingURL(file.content)
|
|
375
|
-
if (!sourceMappingURL) {
|
|
376
|
-
return dependencyFileName
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const sourceMapFileName = toPosixFileName(
|
|
380
|
-
path.resolve(path.dirname(dependencyFileName), sourceMappingURL),
|
|
381
|
-
)
|
|
382
|
-
const sourceMap = this.fs.getFile(sourceMapFileName)
|
|
383
|
-
if (!sourceMap) {
|
|
384
|
-
this.log.warn(`Could not find sourcemap ${sourceMapFileName}`)
|
|
385
|
-
return dependencyFileName
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const sourceMapParsed = JSON.parse(sourceMap.content) as {
|
|
389
|
-
sources?: string[]
|
|
390
|
-
}
|
|
391
|
-
const sources = sourceMapParsed.sources
|
|
392
|
-
|
|
393
|
-
if (sources?.length === 1) {
|
|
394
|
-
const [sourcePath] = sources
|
|
395
|
-
return toPosixFileName(
|
|
396
|
-
path.resolve(path.dirname(sourceMapFileName), sourcePath!),
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return dependencyFileName
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private guardTSConfigFileExists(): void {
|
|
404
|
-
try {
|
|
405
|
-
readFileSync(this.tsconfigFile, 'utf-8')
|
|
406
|
-
} catch {
|
|
407
|
-
throw new Error(
|
|
408
|
-
`The tsconfig file does not exist at: "${this.tsconfigFile}". Please configure the tsconfig file in your stryker.conf file using "tsconfigFile"`,
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import { fileURLToPath } from 'url'
|
|
3
|
-
|
|
4
|
-
import { CheckStatus } from '@stryker-mutator/api/check'
|
|
5
|
-
import type { Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
6
|
-
import type { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging'
|
|
7
|
-
import { commonTokens } from '@stryker-mutator/api/plugin'
|
|
8
|
-
import type { Injector, PluginContext } from '@stryker-mutator/api/plugin'
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
10
|
-
|
|
11
|
-
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
12
|
-
import { createTypescriptChecker } from '../../src/index.js'
|
|
13
|
-
import * as pluginTokens from '../../src/plugin-tokens.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
|
-
interface InjectableFunctionWithTokens {
|
|
44
|
-
inject?: readonly string[]
|
|
45
|
-
(...args: unknown[]): unknown
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface InjectableClassWithTokens {
|
|
49
|
-
inject?: readonly string[]
|
|
50
|
-
new(...args: unknown[]): unknown
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function createInjector(tsconfigFile: string): Injector<PluginContext> {
|
|
54
|
-
const context: Record<string, unknown> = {
|
|
55
|
-
[commonTokens.getLogger]: (): Logger => createLogger(),
|
|
56
|
-
[commonTokens.logger]: createLogger(),
|
|
57
|
-
[commonTokens.options]: {
|
|
58
|
-
tsconfigFile,
|
|
59
|
-
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
60
|
-
} as unknown as StrykerOptions,
|
|
61
|
-
[commonTokens.target]: undefined,
|
|
62
|
-
[commonTokens.injector]: undefined,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const resolveToken = (token: string): unknown => {
|
|
66
|
-
if (!(token in context)) {
|
|
67
|
-
throw new Error(`Missing injection token: ${token}`)
|
|
68
|
-
}
|
|
69
|
-
return context[token]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const injectArgs = (inject: readonly string[]): unknown[] => inject.map(resolveToken)
|
|
73
|
-
|
|
74
|
-
const self: Injector<PluginContext> = {
|
|
75
|
-
provideFactory: (token, factory, _scope) => {
|
|
76
|
-
const f = factory as InjectableFunctionWithTokens
|
|
77
|
-
context[token] = f(...injectArgs(f.inject ?? []))
|
|
78
|
-
return self
|
|
79
|
-
},
|
|
80
|
-
provideClass: (token, Class, _scope) => {
|
|
81
|
-
const C = Class as InjectableClassWithTokens
|
|
82
|
-
context[token] = new C(...injectArgs(C.inject ?? []))
|
|
83
|
-
return self
|
|
84
|
-
},
|
|
85
|
-
injectClass: (Class) => {
|
|
86
|
-
const C = Class as InjectableClassWithTokens
|
|
87
|
-
return new C(...injectArgs(C.inject ?? [])) as never
|
|
88
|
-
},
|
|
89
|
-
injectFunction: (fn) => {
|
|
90
|
-
const f = fn as InjectableFunctionWithTokens
|
|
91
|
-
return f(...injectArgs(f.inject ?? [])) as never
|
|
92
|
-
},
|
|
93
|
-
resolve: (token) => resolveToken(token as string) as never,
|
|
94
|
-
provideValue: (token, value) => {
|
|
95
|
-
context[token] = value
|
|
96
|
-
return self
|
|
97
|
-
},
|
|
98
|
-
createChildInjector: () => self,
|
|
99
|
-
dispose: async () => {},
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return self
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface ClosableChecker {
|
|
106
|
-
tsCompiler: { close(): void }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const todoSource =
|
|
110
|
-
'export interface ITodo {\n name: string;\n description: string;\n completed: boolean;\n}\n\nclass Todo implements ITodo {\n constructor(public name: string, public description: string, public completed: boolean) { }\n}\n\nexport class TodoList {\n public static allTodos: Todo[] = [];\n createTodoItem(name: string, description: string) {\n let newItem = new Todo(name, description, false);\n let totalCount: number = TodoList.allTodos.push(newItem);\n return totalCount;\n }\n\n allTodoItems(): ITodo[] {\n return TodoList.allTodos;\n }\n}\n\n'
|
|
111
|
-
|
|
112
|
-
function createMutant(replacement: string, id: string): Mutant {
|
|
113
|
-
const findText = 'TodoList.allTodos.push(newItem)'
|
|
114
|
-
const lines = todoSource.split('\n')
|
|
115
|
-
const lineNumber = lines.findIndex((line) => line.includes(findText))
|
|
116
|
-
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
117
|
-
return {
|
|
118
|
-
id,
|
|
119
|
-
fileName: resolveTestResource('src', 'todo.ts'),
|
|
120
|
-
mutatorName: 'foo-mutator',
|
|
121
|
-
location: {
|
|
122
|
-
start: { line: lineNumber, column: textColumn },
|
|
123
|
-
end: { line: lineNumber, column: textColumn + findText.length },
|
|
124
|
-
},
|
|
125
|
-
replacement,
|
|
126
|
-
} as unknown as Mutant
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
describe('Typescript checker plugin entry', () => {
|
|
130
|
-
let sut: TypescriptChecker
|
|
131
|
-
|
|
132
|
-
beforeEach(() => {
|
|
133
|
-
const injector = createInjector(resolveTestResource('tsconfig.json'))
|
|
134
|
-
sut = createTypescriptChecker(injector)
|
|
135
|
-
return sut.init()
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
afterEach(() => {
|
|
139
|
-
;(sut as unknown as ClosableChecker).tsCompiler.close()
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('should report Passed for a mutant without compile errors', async () => {
|
|
143
|
-
const mutant = createMutant('newItem ? 42 : 43', 'e2e-pass')
|
|
144
|
-
const actual = await sut.check([mutant])
|
|
145
|
-
expect(actual).toEqual({ 'e2e-pass': { status: CheckStatus.Passed } })
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('should report CompileError for a mutant that introduces a type error', async () => {
|
|
149
|
-
const mutant = createMutant('TodoList.allTodos.push("invalid")', 'e2e-fail')
|
|
150
|
-
const actual = await sut.check([mutant])
|
|
151
|
-
expect(actual['e2e-fail']?.status).toBe(CheckStatus.CompileError)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
@@ -1,101 +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 { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
7
|
-
import type { Logger } from '@stryker-mutator/api/logging'
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
9
|
-
|
|
10
|
-
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
11
|
-
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
12
|
-
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
13
|
-
|
|
14
|
-
const resolveTestResource = path.resolve.bind(
|
|
15
|
-
path,
|
|
16
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
-
'..',
|
|
18
|
-
'..',
|
|
19
|
-
'testResources',
|
|
20
|
-
'nodenext-project',
|
|
21
|
-
) as unknown as typeof path.resolve
|
|
22
|
-
|
|
23
|
-
function createLogger(): Logger {
|
|
24
|
-
return {
|
|
25
|
-
isTraceEnabled: () => false,
|
|
26
|
-
isDebugEnabled: () => false,
|
|
27
|
-
isInfoEnabled: () => false,
|
|
28
|
-
isWarnEnabled: () => false,
|
|
29
|
-
isErrorEnabled: () => false,
|
|
30
|
-
isFatalEnabled: () => false,
|
|
31
|
-
trace: () => {},
|
|
32
|
-
debug: () => {},
|
|
33
|
-
info: () => {},
|
|
34
|
-
warn: () => {},
|
|
35
|
-
error: () => {},
|
|
36
|
-
fatal: () => {},
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
41
|
-
const options = {
|
|
42
|
-
tsconfigFile,
|
|
43
|
-
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
44
|
-
} as unknown as StrykerOptions
|
|
45
|
-
const logger = createLogger()
|
|
46
|
-
const fileSystem = new HybridFileSystem()
|
|
47
|
-
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
48
|
-
return new TypescriptChecker(logger, options, compiler)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const utilSource = fs.readFileSync(resolveTestResource('src', 'util.ts'), 'utf8')
|
|
52
|
-
|
|
53
|
-
function createMutant(
|
|
54
|
-
findText: string,
|
|
55
|
-
replacement: string,
|
|
56
|
-
id: string,
|
|
57
|
-
): Mutant {
|
|
58
|
-
const lines = utilSource.split('\n')
|
|
59
|
-
const lineNumber = lines.findIndex((line) => line.includes(findText))
|
|
60
|
-
if (lineNumber === -1) {
|
|
61
|
-
throw new Error(`Cannot find ${findText} in util.ts`)
|
|
62
|
-
}
|
|
63
|
-
const column = lines[lineNumber]!.indexOf(findText)
|
|
64
|
-
const location: Location = {
|
|
65
|
-
start: { line: lineNumber, column },
|
|
66
|
-
end: { line: lineNumber, column: column + findText.length },
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
id,
|
|
70
|
-
fileName: resolveTestResource('src', 'util.ts'),
|
|
71
|
-
mutatorName: 'test-mutator',
|
|
72
|
-
location,
|
|
73
|
-
replacement,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
describe('Typescript checker on a NodeNext project targeting es2024', () => {
|
|
78
|
-
let sut: TypescriptChecker
|
|
79
|
-
|
|
80
|
-
beforeEach(() => {
|
|
81
|
-
sut = createChecker(resolveTestResource('tsconfig.json'))
|
|
82
|
-
return sut.init()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
// @ts-expect-error private close method
|
|
87
|
-
sut.tsCompiler.close()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('should validate a mutant that keeps the project compiling', async () => {
|
|
91
|
-
const mutant = createMutant('value % 2 === 0', 'value % 2 !== 0', 'passing')
|
|
92
|
-
const actual = await sut.check([mutant])
|
|
93
|
-
expect(actual).toEqual({ passing: { status: CheckStatus.Passed } })
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('should invalidate a mutant that violates the declared return type', async () => {
|
|
97
|
-
const mutant = createMutant("'even'", '42', 'breaking')
|
|
98
|
-
const actual = await sut.check([mutant])
|
|
99
|
-
expect(actual['breaking']!.status).toBe(CheckStatus.CompileError)
|
|
100
|
-
})
|
|
101
|
-
})
|