@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,77 +0,0 @@
|
|
|
1
|
-
import type { Mutant } from '@stryker-mutator/api/core'
|
|
2
|
-
|
|
3
|
-
import { toPosixFileName } from '../tsconfig-helpers.js'
|
|
4
|
-
|
|
5
|
-
import { TSFileNode } from './ts-file-node.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* To speed up the type-checking we want to check multiple mutants at once.
|
|
9
|
-
* When multiple mutants in different files don't have overlap in affected files (or have small overlap), we can type-check them simultaneously.
|
|
10
|
-
* These mutants who can be tested at the same time are called a group.
|
|
11
|
-
* Therefore, the return type is an array of arrays, in other words: an array of groups.
|
|
12
|
-
*
|
|
13
|
-
* @param mutants All the mutants of the test project.
|
|
14
|
-
* @param nodes A graph representation of the test project.
|
|
15
|
-
*/
|
|
16
|
-
export function createGroups(
|
|
17
|
-
mutants: Mutant[],
|
|
18
|
-
nodes: Map<string, TSFileNode>,
|
|
19
|
-
): string[][] {
|
|
20
|
-
const groups: string[][] = []
|
|
21
|
-
const mutantsToGroup = new Set(mutants)
|
|
22
|
-
|
|
23
|
-
while (mutantsToGroup.size) {
|
|
24
|
-
const group: string[] = []
|
|
25
|
-
const groupNodes = new Set<TSFileNode>()
|
|
26
|
-
const nodesToIgnore = new Set<TSFileNode>()
|
|
27
|
-
|
|
28
|
-
for (const currentMutant of mutantsToGroup) {
|
|
29
|
-
const currentNode = findNode(currentMutant.fileName, nodes)
|
|
30
|
-
if (
|
|
31
|
-
!nodesToIgnore.has(currentNode) &&
|
|
32
|
-
!parentsHaveOverlapWith(currentNode, groupNodes)
|
|
33
|
-
) {
|
|
34
|
-
group.push(currentMutant.id)
|
|
35
|
-
groupNodes.add(currentNode)
|
|
36
|
-
mutantsToGroup.delete(currentMutant)
|
|
37
|
-
addRangeOfNodesToSet(
|
|
38
|
-
nodesToIgnore,
|
|
39
|
-
currentNode.getAllParentReferencesIncludingSelf(),
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
groups.push(group)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return groups
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function addRangeOfNodesToSet(
|
|
50
|
-
nodes: Set<TSFileNode>,
|
|
51
|
-
nodesToAdd: Iterable<TSFileNode>,
|
|
52
|
-
) {
|
|
53
|
-
for (const parent of nodesToAdd) {
|
|
54
|
-
nodes.add(parent)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function findNode(fileName: string, nodes: Map<string, TSFileNode>) {
|
|
59
|
-
const node = nodes.get(toPosixFileName(fileName))
|
|
60
|
-
if (node == null) {
|
|
61
|
-
throw new Error(`Node not in graph: ${fileName}`)
|
|
62
|
-
}
|
|
63
|
-
return node
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parentsHaveOverlapWith(
|
|
67
|
-
currentNode: TSFileNode,
|
|
68
|
-
groupNodes: Set<TSFileNode>,
|
|
69
|
-
) {
|
|
70
|
-
for (const parentNode of currentNode.getAllParentReferencesIncludingSelf()) {
|
|
71
|
-
if (groupNodes.has(parentNode)) {
|
|
72
|
-
return true
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { Mutant } from '@stryker-mutator/api/core'
|
|
2
|
-
|
|
3
|
-
import { toPosixFileName } from '../tsconfig-helpers.js'
|
|
4
|
-
|
|
5
|
-
// This class exists so we can have a two-way dependency graph.
|
|
6
|
-
// The two-way dependency graph is used to search for mutants related to typescript errors.
|
|
7
|
-
export class TSFileNode {
|
|
8
|
-
constructor(
|
|
9
|
-
public fileName: string,
|
|
10
|
-
public parents: TSFileNode[],
|
|
11
|
-
public children: TSFileNode[],
|
|
12
|
-
) {}
|
|
13
|
-
|
|
14
|
-
public getAllParentReferencesIncludingSelf(
|
|
15
|
-
allParentReferences: Set<TSFileNode> = new Set<TSFileNode>(),
|
|
16
|
-
): Set<TSFileNode> {
|
|
17
|
-
allParentReferences.add(this)
|
|
18
|
-
this.parents.forEach((parent) => {
|
|
19
|
-
if (!allParentReferences.has(parent)) {
|
|
20
|
-
parent.getAllParentReferencesIncludingSelf(allParentReferences)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
return allParentReferences
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public getAllChildReferencesIncludingSelf(
|
|
27
|
-
allChildReferences: Set<TSFileNode> = new Set<TSFileNode>(),
|
|
28
|
-
): Set<TSFileNode> {
|
|
29
|
-
allChildReferences.add(this)
|
|
30
|
-
this.children.forEach((child) => {
|
|
31
|
-
if (!allChildReferences.has(child)) {
|
|
32
|
-
child.getAllChildReferencesIncludingSelf(allChildReferences)
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
return allChildReferences
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
public getMutantsWithReferenceToChildrenOrSelf(
|
|
39
|
-
mutants: Mutant[],
|
|
40
|
-
nodesChecked: string[] = [],
|
|
41
|
-
): Mutant[] {
|
|
42
|
-
if (nodesChecked.includes(this.fileName)) {
|
|
43
|
-
return []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
nodesChecked.push(this.fileName)
|
|
47
|
-
|
|
48
|
-
const relatedMutants = mutants.filter(
|
|
49
|
-
(m) => toPosixFileName(m.fileName) === this.fileName,
|
|
50
|
-
)
|
|
51
|
-
const childResult = this.children.flatMap((c) => c.getMutantsWithReferenceToChildrenOrSelf(mutants, nodesChecked))
|
|
52
|
-
return [...relatedMutants, ...childResult]
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs'
|
|
2
|
-
|
|
3
|
-
import { declareFactoryPlugin, PluginKind } from '@stryker-mutator/api/plugin'
|
|
4
|
-
|
|
5
|
-
import { create } from './typescript-checker.js'
|
|
6
|
-
|
|
7
|
-
export const strykerPlugins = [
|
|
8
|
-
declareFactoryPlugin(PluginKind.Checker, 'typescript', create),
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
export const createTypescriptChecker = create
|
|
12
|
-
|
|
13
|
-
export const strykerValidationSchema: Record<string, unknown> = JSON.parse(
|
|
14
|
-
readFileSync(
|
|
15
|
-
new URL('../schema/typescript-checker-options.json', import.meta.url),
|
|
16
|
-
'utf-8',
|
|
17
|
-
),
|
|
18
|
-
)
|
package/src/plugin-tokens.ts
DELETED
package/src/tsconfig-helpers.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs'
|
|
2
|
-
import { createRequire } from 'module'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
import { parse } from '@std/jsonc'
|
|
6
|
-
import { Data, Either, Schema as S } from 'effect'
|
|
7
|
-
import semver from 'semver'
|
|
8
|
-
|
|
9
|
-
// Override some compiler options that have to do with code quality. When mutating, we're not interested in the resulting code quality
|
|
10
|
-
// See https://github.com/stryker-mutator/stryker-js/issues/391 for more info
|
|
11
|
-
const COMPILER_OPTIONS_OVERRIDES: Readonly<Record<string, unknown>> = Object.freeze({
|
|
12
|
-
allowUnreachableCode: true,
|
|
13
|
-
noUnusedLocals: false,
|
|
14
|
-
noUnusedParameters: false,
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
// When we're running in 'single-project' mode, we can safely disable emit
|
|
18
|
-
const NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT: Readonly<Record<string, unknown>> = Object.freeze({
|
|
19
|
-
noEmit: true,
|
|
20
|
-
incremental: false, // incremental and composite off: https://github.com/microsoft/TypeScript/issues/36917
|
|
21
|
-
tsBuildInfoFile: undefined,
|
|
22
|
-
composite: false,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// When we're running in 'project references' mode, we need to enable declaration output
|
|
26
|
-
const LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES: Readonly<Record<string, unknown>> = Object.freeze({
|
|
27
|
-
emitDeclarationOnly: true,
|
|
28
|
-
noEmit: false,
|
|
29
|
-
declarationMap: true,
|
|
30
|
-
declaration: true,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
let cachedTSVersion: string | undefined
|
|
34
|
-
|
|
35
|
-
export function getTSVersion(): string {
|
|
36
|
-
if (cachedTSVersion === undefined) {
|
|
37
|
-
const require = createRequire(import.meta.url)
|
|
38
|
-
const pkg = JSON.parse(
|
|
39
|
-
readFileSync(require.resolve('typescript/package.json'), 'utf-8'),
|
|
40
|
-
) as { version: string }
|
|
41
|
-
cachedTSVersion = pkg.version
|
|
42
|
-
}
|
|
43
|
-
return cachedTSVersion
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function guardTSVersion(version = getTSVersion()): void {
|
|
47
|
-
if (!semver.satisfies(version, '>=7.0.0', { includePrerelease: true })) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`@systemfsoftware/stryker-js-typescript-checker only supports typescript@7.0.0 or higher. Found typescript@${version}`,
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Error returned when a tsconfig file fails to parse or does not match the shape this package consumes.
|
|
56
|
-
*/
|
|
57
|
-
export class TsConfigParseError extends Data.TaggedError('TsConfigParseError')<{
|
|
58
|
-
readonly file: string
|
|
59
|
-
readonly reason: string
|
|
60
|
-
}> {}
|
|
61
|
-
|
|
62
|
-
const JsonRecord = S.Record({ key: S.String, value: S.Unknown })
|
|
63
|
-
const TsConfigSchema = S.Struct(
|
|
64
|
-
{
|
|
65
|
-
references: S.optional(S.Array(S.Struct({ path: S.String }, JsonRecord))),
|
|
66
|
-
compilerOptions: S.optional(JsonRecord),
|
|
67
|
-
},
|
|
68
|
-
JsonRecord,
|
|
69
|
-
)
|
|
70
|
-
type TsConfig = S.Schema.Type<typeof TsConfigSchema>
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Parses the raw text of a tsconfig file into a typed config, rejecting shapes this package cannot consume.
|
|
74
|
-
* @param fileName The tsconfig file name, used for error reporting
|
|
75
|
-
* @param jsonText The raw tsconfig content
|
|
76
|
-
*/
|
|
77
|
-
export function parseTsConfig(fileName: string, jsonText: string): Either.Either<TsConfig, TsConfigParseError> {
|
|
78
|
-
// `@std/jsonc`'s whitespace set excludes U+FEFF, so it rejects a leading BOM, while `tsc` tolerates one.
|
|
79
|
-
try {
|
|
80
|
-
const value = parse(jsonText.replace(/^\uFEFF/, ''))
|
|
81
|
-
// Rebuilds the object, reordering keys (declared fields hoist). Safe here: this
|
|
82
|
-
// package only reads the config, and `overrideOptions` builds a fresh object anyway.
|
|
83
|
-
// The core sibling (`packages/stryker-js/core/src/sandbox/parse-config-helper.ts`)
|
|
84
|
-
// deliberately uses an `S.is` guard instead — it mutates the parsed config and writes
|
|
85
|
-
// it back with `JSON.stringify`, so a rebuild there would reorder the user's tsconfig
|
|
86
|
-
// on disk. Independent boundaries by KTD-2; this note keeps the divergence deliberate.
|
|
87
|
-
return Either.mapLeft(
|
|
88
|
-
S.decodeUnknownEither(TsConfigSchema)(value),
|
|
89
|
-
(issue) => new TsConfigParseError({ file: fileName, reason: issue.message }),
|
|
90
|
-
)
|
|
91
|
-
} catch (error) {
|
|
92
|
-
return Either.left(
|
|
93
|
-
new TsConfigParseError({
|
|
94
|
-
file: fileName,
|
|
95
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
96
|
-
}),
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Determines whether or not to use `--build` mode based on "references" being there in the config file
|
|
103
|
-
* @param tsconfigFileName The tsconfig file to parse
|
|
104
|
-
*/
|
|
105
|
-
export function determineBuildModeEnabled(tsconfigFileName: string): boolean {
|
|
106
|
-
const tsconfigFile = readFileSync(tsconfigFileName, 'utf-8')
|
|
107
|
-
const parsed = parseTsConfig(tsconfigFileName, tsconfigFile)
|
|
108
|
-
return Either.match(parsed, {
|
|
109
|
-
onLeft: () => false,
|
|
110
|
-
onRight: (config) => config.references !== undefined,
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Overrides some options to speed up compilation and disable some code quality checks we don't want during mutation testing
|
|
116
|
-
* @param config The parsed config file
|
|
117
|
-
* @param useBuildMode whether or not `--build` mode is used
|
|
118
|
-
*/
|
|
119
|
-
export function overrideOptions(config: TsConfig, useBuildMode: boolean): string {
|
|
120
|
-
// `target` and `moduleResolution` are deliberately absent: both belong to the consumer.
|
|
121
|
-
// Forcing `moduleResolution` contradicts `module: NodeNext`/`Node16` (TS5095 + TS5109),
|
|
122
|
-
// and forcing `target` hides lib features the consumer's own target allows (TS2550).
|
|
123
|
-
const compilerOptions: Record<string, unknown> = {
|
|
124
|
-
...config.compilerOptions,
|
|
125
|
-
...COMPILER_OPTIONS_OVERRIDES,
|
|
126
|
-
...(useBuildMode ? LOW_EMIT_OPTIONS_FOR_PROJECT_REFERENCES : NO_EMIT_OPTIONS_FOR_SINGLE_PROJECT),
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
!useBuildMode &&
|
|
131
|
-
compilerOptions['declarationDir'] !== undefined &&
|
|
132
|
-
compilerOptions['declarationDir'] !== null
|
|
133
|
-
) {
|
|
134
|
-
// because composite and/or declaration was disabled in non-build mode, we have to disable declarationDir as well
|
|
135
|
-
// otherwise, error TS5069: Option 'declarationDir' cannot be specified without specifying option 'declaration' or option 'composite'.
|
|
136
|
-
delete compilerOptions['declarationDir']
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (useBuildMode) {
|
|
140
|
-
// Remove the options to place declarations files in different locations to decrease the complexity of searching the source file in the TypescriptCompiler class.
|
|
141
|
-
delete compilerOptions['inlineSourceMap']
|
|
142
|
-
delete compilerOptions['inlineSources']
|
|
143
|
-
delete compilerOptions['mapRoute']
|
|
144
|
-
delete compilerOptions['sourceRoot']
|
|
145
|
-
delete compilerOptions['outFile']
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return JSON.stringify({
|
|
149
|
-
...config,
|
|
150
|
-
compilerOptions,
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Retrieves the referenced config files based on parsed configuration
|
|
156
|
-
* @param config The parsed config file
|
|
157
|
-
* @param fromDirName The directory where to resolve from
|
|
158
|
-
*/
|
|
159
|
-
export function retrieveReferencedProjects(config: TsConfig, fromDirName: string): string[] {
|
|
160
|
-
return (config.references ?? []).map((reference) => {
|
|
161
|
-
let resolved = path.resolve(fromDirName, reference.path)
|
|
162
|
-
if (!path.basename(resolved).endsWith('.json')) {
|
|
163
|
-
resolved = path.join(resolved, 'tsconfig.json')
|
|
164
|
-
}
|
|
165
|
-
return toPosixFileName(resolved)
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Replaces backslashes with forward slashes (used by typescript)
|
|
171
|
-
* @param fileName The file name that may contain backslashes `\`
|
|
172
|
-
* @returns posix and ts complaint file name (with `/`)
|
|
173
|
-
*/
|
|
174
|
-
export function toPosixFileName(fileName: string): string {
|
|
175
|
-
return fileName.replace(/\\/g, '/')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Find source file in declaration file
|
|
180
|
-
* @param content The content of the declaration file
|
|
181
|
-
* @returns URL of the source file or undefined if not found
|
|
182
|
-
*/
|
|
183
|
-
const findSourceMapRegex = /\/\/# sourceMappingURL=(.+)$/m
|
|
184
|
-
export function getSourceMappingURL(content: string): string | undefined {
|
|
185
|
-
findSourceMapRegex.lastIndex = 0
|
|
186
|
-
return findSourceMapRegex.exec(content)?.[1]
|
|
187
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { StrykerOptions } from '@stryker-mutator/api/core'
|
|
2
|
-
|
|
3
|
-
export interface TypescriptCheckerPluginOptions {
|
|
4
|
-
typescriptChecker: {
|
|
5
|
-
prioritizePerformanceOverAccuracy?: boolean
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface TypescriptCheckerOptionsWithStrykerOptions extends TypescriptCheckerPluginOptions, StrykerOptions {}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { EOL } from 'os'
|
|
2
|
-
|
|
3
|
-
import type { Checker, CheckResult } from '@stryker-mutator/api/check'
|
|
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, Scope, tokens } from '@stryker-mutator/api/plugin'
|
|
8
|
-
import type { Injector, PluginContext } from '@stryker-mutator/api/plugin'
|
|
9
|
-
import { split, strykerReportBugUrl } from '@stryker-mutator/util'
|
|
10
|
-
import { DiagnosticCategory } from 'typescript/unstable/sync'
|
|
11
|
-
import type { Diagnostic } from 'typescript/unstable/sync'
|
|
12
|
-
|
|
13
|
-
import { HybridFileSystem } from './fs/hybrid-file-system.js'
|
|
14
|
-
import { createGroups } from './grouping/create-groups.js'
|
|
15
|
-
import { TSFileNode } from './grouping/ts-file-node.js'
|
|
16
|
-
import * as pluginTokens from './plugin-tokens.js'
|
|
17
|
-
import { toPosixFileName } from './tsconfig-helpers.js'
|
|
18
|
-
import type { TypescriptCheckerOptionsWithStrykerOptions } from './typescript-checker-options-with-stryker-options.js'
|
|
19
|
-
import { TypescriptCompiler } from './typescript-compiler.js'
|
|
20
|
-
|
|
21
|
-
const typescriptCheckerLoggerFactory = Object.assign(
|
|
22
|
-
(
|
|
23
|
-
loggerFactory: LoggerFactoryMethod,
|
|
24
|
-
target: Function | undefined,
|
|
25
|
-
): Logger => {
|
|
26
|
-
const targetName = target?.name ?? TypescriptChecker.name
|
|
27
|
-
const category = targetName === TypescriptChecker.name
|
|
28
|
-
? TypescriptChecker.name
|
|
29
|
-
: `${TypescriptChecker.name}.${targetName}`
|
|
30
|
-
return loggerFactory(category)
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
inject: tokens(commonTokens.getLogger, commonTokens.target),
|
|
34
|
-
},
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
export const create = Object.assign(
|
|
38
|
-
(injector: Injector<PluginContext>): TypescriptChecker =>
|
|
39
|
-
injector
|
|
40
|
-
.provideFactory(
|
|
41
|
-
commonTokens.logger,
|
|
42
|
-
typescriptCheckerLoggerFactory,
|
|
43
|
-
Scope.Transient,
|
|
44
|
-
)
|
|
45
|
-
.provideClass(pluginTokens.fs, HybridFileSystem)
|
|
46
|
-
.provideClass(pluginTokens.tsCompiler, TypescriptCompiler)
|
|
47
|
-
.injectClass(TypescriptChecker),
|
|
48
|
-
{
|
|
49
|
-
inject: tokens(commonTokens.injector),
|
|
50
|
-
},
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
export class TypescriptChecker implements Checker {
|
|
54
|
-
public static inject = tokens(
|
|
55
|
-
commonTokens.logger,
|
|
56
|
-
commonTokens.options,
|
|
57
|
-
pluginTokens.tsCompiler,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
private readonly options: TypescriptCheckerOptionsWithStrykerOptions
|
|
61
|
-
|
|
62
|
-
constructor(
|
|
63
|
-
private readonly logger: Logger,
|
|
64
|
-
options: StrykerOptions,
|
|
65
|
-
private readonly tsCompiler: TypescriptCompiler,
|
|
66
|
-
) {
|
|
67
|
-
this.options = options as TypescriptCheckerOptionsWithStrykerOptions
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public async init(): Promise<void> {
|
|
71
|
-
const errors = await this.tsCompiler.init()
|
|
72
|
-
|
|
73
|
-
if (errors.length) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Typescript error(s) found in dry run compilation: ${this.createErrorText(errors)}`,
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
public async check(mutants: Mutant[]): Promise<Record<string, CheckResult>> {
|
|
81
|
-
const result: Record<string, CheckResult> = Object.fromEntries(
|
|
82
|
-
mutants.map((mutant) => [mutant.id, { status: CheckStatus.Passed }]),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
// Check if this is the group with unrelated files and return check status passed if so
|
|
86
|
-
if (!this.tsCompiler.nodes.get(toPosixFileName(mutants[0]!.fileName))) {
|
|
87
|
-
return result
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const mutantErrorRelationMap = await this.checkErrors(
|
|
91
|
-
mutants,
|
|
92
|
-
{},
|
|
93
|
-
this.tsCompiler.nodes,
|
|
94
|
-
)
|
|
95
|
-
for (const [id, errors] of Object.entries(mutantErrorRelationMap)) {
|
|
96
|
-
result[id] = {
|
|
97
|
-
status: CheckStatus.CompileError,
|
|
98
|
-
reason: this.createErrorText(errors),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return result
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public group(mutants: Mutant[]): Promise<string[][]> {
|
|
106
|
-
if (!this.options.typescriptChecker?.prioritizePerformanceOverAccuracy) {
|
|
107
|
-
return Promise.resolve(mutants.map((m) => [m.id]))
|
|
108
|
-
}
|
|
109
|
-
const { nodes } = this.tsCompiler
|
|
110
|
-
const [mutantsOutsideProject, mutantsInProject] = split(
|
|
111
|
-
mutants,
|
|
112
|
-
(m) => nodes.get(toPosixFileName(m.fileName)) == null,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
const groups = createGroups(mutantsInProject, nodes)
|
|
116
|
-
if (mutantsOutsideProject.length) {
|
|
117
|
-
return Promise.resolve([
|
|
118
|
-
mutantsOutsideProject.map((m) => m.id),
|
|
119
|
-
...groups,
|
|
120
|
-
])
|
|
121
|
-
} else {
|
|
122
|
-
return Promise.resolve(groups)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private async checkErrors(
|
|
127
|
-
mutants: Mutant[],
|
|
128
|
-
errorsMap: Record<string, Diagnostic[]>,
|
|
129
|
-
nodes: Map<string, TSFileNode>,
|
|
130
|
-
): Promise<Record<string, Diagnostic[]>> {
|
|
131
|
-
const errors = await this.tsCompiler.check(mutants)
|
|
132
|
-
const mutantsThatCouldNotBeTestedInGroups = new Set<Mutant>()
|
|
133
|
-
|
|
134
|
-
// If there is only a single mutant the error has to originate from the single mutant
|
|
135
|
-
if (errors.length && mutants.length === 1) {
|
|
136
|
-
errorsMap[mutants[0]!.id] = errors
|
|
137
|
-
return errorsMap
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
for (const error of errors) {
|
|
141
|
-
if (!error.fileName) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Typescript error: '${error.text}' was reported without a corresponding file. This shouldn't happen. Please open an issue using this link: ${
|
|
144
|
-
strykerReportBugUrl(
|
|
145
|
-
`[BUG]: TypeScript checker reports compile error without a corresponding file: ${error.text}`,
|
|
146
|
-
)
|
|
147
|
-
}`,
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
const nodeErrorWasThrownIn = nodes.get(error.fileName)
|
|
151
|
-
if (!nodeErrorWasThrownIn) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Typescript error: '${error.text}' was reported in an unrelated file (${error.fileName}). This file is not part of your project, or referenced from your project. This shouldn't happen, please open an issue using this link: ${
|
|
154
|
-
strykerReportBugUrl(
|
|
155
|
-
`[BUG]: TypeScript checker reports compile error in an unrelated file: ${error.text}`,
|
|
156
|
-
)
|
|
157
|
-
}`,
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
const mutantsRelatedToError = nodeErrorWasThrownIn.getMutantsWithReferenceToChildrenOrSelf(mutants)
|
|
161
|
-
|
|
162
|
-
if (mutantsRelatedToError.length === 0) {
|
|
163
|
-
// In rare cases there are no mutants related to the typescript error
|
|
164
|
-
// Having to test all mutants individually to know which mutant thrown the error
|
|
165
|
-
for (const mutant of mutants) {
|
|
166
|
-
mutantsThatCouldNotBeTestedInGroups.add(mutant)
|
|
167
|
-
}
|
|
168
|
-
} else if (mutantsRelatedToError.length === 1) {
|
|
169
|
-
// There is only one mutant related to the typescript error so we can add it to the errorsRelatedToMutant
|
|
170
|
-
const mutantId = mutantsRelatedToError[0]!.id
|
|
171
|
-
if (errorsMap[mutantId]) {
|
|
172
|
-
errorsMap[mutantId]!.push(error)
|
|
173
|
-
} else {
|
|
174
|
-
errorsMap[mutantId] = [error]
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
// If there are more than one mutants related to the error we should check them individually
|
|
178
|
-
for (const mutant of mutantsRelatedToError) {
|
|
179
|
-
mutantsThatCouldNotBeTestedInGroups.add(mutant)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (mutantsThatCouldNotBeTestedInGroups.size) {
|
|
185
|
-
// Because at this point the filesystem contains all the mutants from the group we need to reset back
|
|
186
|
-
// to the original state of the files to make it possible to test the first mutant
|
|
187
|
-
// if we wouldn't do this the first mutant would not be noticed by the compiler because it was already in the filesystem
|
|
188
|
-
await this.tsCompiler.check([])
|
|
189
|
-
}
|
|
190
|
-
for (const mutant of mutantsThatCouldNotBeTestedInGroups) {
|
|
191
|
-
if (errorsMap[mutant.id]) continue
|
|
192
|
-
await this.checkErrors([mutant], errorsMap, nodes)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return errorsMap
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private createErrorText(errors: Diagnostic[]): string {
|
|
199
|
-
return errors
|
|
200
|
-
.map((error) => this.formatDiagnostic(error))
|
|
201
|
-
.join(EOL)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private formatDiagnostic(error: Diagnostic): string {
|
|
205
|
-
const severity = error.category === DiagnosticCategory.Error
|
|
206
|
-
? 'error'
|
|
207
|
-
: error.category === DiagnosticCategory.Warning
|
|
208
|
-
? 'warning'
|
|
209
|
-
: error.category === DiagnosticCategory.Suggestion
|
|
210
|
-
? 'suggestion'
|
|
211
|
-
: 'message'
|
|
212
|
-
|
|
213
|
-
let location = ''
|
|
214
|
-
if (error.fileName) {
|
|
215
|
-
const lineAndCharacter = this.tsCompiler.getLineAndCharacterOfPosition(error.fileName, error.pos)
|
|
216
|
-
const line = (lineAndCharacter?.line ?? 0) + 1
|
|
217
|
-
const character = (lineAndCharacter?.character ?? 0) + 1
|
|
218
|
-
location = `${error.fileName}(${line},${character}): `
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return `${location}${severity} TS${error.code}: ${error.text}`
|
|
222
|
-
}
|
|
223
|
-
}
|