Skip to content

Commit

Permalink
feat(disableTypeChecks): add option 'true' to disable all type checks (
Browse files Browse the repository at this point in the history
…#3765)


Co-authored-by: Nico Jansen <jansennico@gmail.com>
  • Loading branch information
simondel and nicojs committed Oct 28, 2022
1 parent c3ce6fe commit 3c3d298
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 67 deletions.
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ As you can see, when you disable bail, a lot more tests get the "Killing" status

_Note: Disable bail needs to be supported by the test runner plugin in order to work. All official test runner plugins (`@stryker-mutator/xxx-runner`) support this feature except for Jest. Jest always runs without --bail (see [#11766](https://github.com/facebook/jest/issues/11766)) inside Stryker, however it will report only the first failing test when disableBail=false and all failing tests when disableBail=true_

### `disableTypeChecks` [`false | string`]
### `disableTypeChecks` [`boolean` | `string`]

Default: `"{test,src,lib}/**/*.{js,ts,jsx,tsx,html,vue}"`<br />
Command: _none_<br />
Config file: `"disableTypeChecks": false`

Configure a pattern that matches the files of which type checking has to be disabled. This is needed because Stryker will create (typescript) type errors when inserting the mutants in your code. Stryker disables type checking by inserting `// @ts-nocheck` atop those files and removing other `// @ts-xxx` directives (so they won't interfere with `@ts-nocheck`). The default setting allows these directives to be stripped from all JavaScript and friend files in `lib`, `src` and `test` directories. You can specify a different glob expression or set it to `false` to completely disable this behavior.
Set to 'true' to disable type checking, or 'false' to enable it. For more control, configure a pattern that matches the files of which type checking has to be disabled. This is needed because Stryker will create (typescript) type errors when inserting the mutants in your code. Stryker disables type checking by inserting `// @ts-nocheck` atop those files and removing other `// @ts-xxx` directives (so they won't interfere with `@ts-nocheck`). The default setting allows these directives to be stripped from all JavaScript and friend files in `lib`, `src` and `test` directories.

### `dryRunTimeoutMinutes` [`number`]

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Example:
> [...] Cannot assign to 'stryNS_9fa48' because it is not a variable [...]
> ```
The initial test run might fail when you're using ts-jest or ts-node. The reason for this is that Stryker will mutate your code and, by doing so, introduce type errors into your code. Stryker tries to ignore these errors by adding [`// @ts-nocheck`](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/#ts-nocheck-in-typescript-files) in your source files. However, this is only done for TypeScript-like files inside your `lib`, `src`, and `test` directories by default. If you have your code somewhere else, you will need to override [disableTypeChecks](./configuration.md#disabletypechecks-false--string) yourself:
The initial test run might fail when you're using ts-jest or ts-node. The reason for this is that Stryker will mutate your code and, by doing so, introduce type errors into your code. Stryker tries to ignore these errors by adding [`// @ts-nocheck`](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/#ts-nocheck-in-typescript-files) in your source files. However, this is only done for TypeScript-like files inside your `lib`, `src`, and `test` directories by default. If you have your code somewhere else, you will need to override [disableTypeChecks](./configuration.md#disabletypechecks-false--true--string) yourself:

```json
{
Expand Down
2 changes: 1 addition & 1 deletion e2e/test/typescript-transpiling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "tsc",
"pretest:unit": "npm run build",
"test:unit": "mocha",
"pretest": "rimraf \"reports\" \"dist\"",
"pretest": "rimraf \"reports\" \"dist\" \"stryker.log\"",
"test": "stryker run",
"posttest": "mocha --no-config --no-package --timeout 0 verify/verify.js"
},
Expand Down
2 changes: 2 additions & 0 deletions e2e/test/typescript-transpiling/stryker.conf.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"packageManager": "npm",
"disableTypeChecks": true,
"testRunner": "mocha",
"concurrency": 1,
"coverageAnalysis": "perTest",
"reporters": ["json", "html", "progress", "clear-text"],
"checkers": ["typescript"],
"tsconfigFile": "tsconfig.json",
"fileLogLevel": "warn",
"buildCommand": "npm run build",
"plugins": [
"@stryker-mutator/mocha-runner",
Expand Down
2 changes: 1 addition & 1 deletion e2e/test/typescript-transpiling/test/AddSpec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { add, addOne, isNegativeNumber, notCovered, negate } from '../src/Add';
import { add, addOne, isNegativeNumber, negate } from '../src/Add';

describe('Add', () => {
it('should be able to add two numbers', () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,12 @@
"default": {}
},
"disableTypeChecks": {
"description": "Configure a pattern that matches the files of which type checking has to be disabled. This is needed because Stryker will create (typescript) type errors when inserting the mutants in your code. Stryker disables type checking by inserting `// @ts-nocheck` atop those files and removing other `// @ts-xxx` directives (so they won't interfere with `@ts-nocheck`). The default setting allows these directives to be stripped from all JavaScript and friend files in `lib`, `src` and `test` directories. You can specify a different glob expression or set it to `false` to completely disable this behavior.",
"description": "Set to 'true' to disable type checking, or 'false' to enable it. For more control, configure a pattern that matches the files of which type checking has to be disabled. This is needed because Stryker will create (typescript) type errors when inserting the mutants in your code. Stryker disables type checking by inserting `// @ts-nocheck` atop those files and removing other `// @ts-xxx` directives (so they won't interfere with `@ts-nocheck`). The default setting allows these directives to be stripped from all JavaScript and friend files in `lib`, `src` and `test` directories.",
"oneOf": [
{
"enum": [
false
false,
true
]
},
{
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/config/file-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import { normalizeFileName } from '@stryker-mutator/util';
* A helper class for matching files using the `disableTypeChecks` setting.
*/
export class FileMatcher {
private readonly pattern: string | false;
private readonly pattern: boolean | string;

constructor(pattern: string | false) {
if (pattern !== false) {
constructor(pattern: boolean | string) {
if (typeof pattern === 'string') {
this.pattern = normalizeFileName(path.resolve(pattern));
} else {
this.pattern = pattern;
}
}

public matches(fileName: string): boolean {
return !!this.pattern && minimatch(normalizeFileName(path.resolve(fileName)), this.pattern);
if (typeof this.pattern === 'string') {
return minimatch(normalizeFileName(path.resolve(fileName)), this.pattern);
} else {
return this.pattern;
}
}
}
5 changes: 5 additions & 0 deletions packages/core/test/unit/config/file-matcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ describe(FileMatcher.name, () => {
expect(sut.matches('src/foo.ts')).true;
});

it('should match if the pattern is set to `true`', () => {
const sut = new FileMatcher(true);
expect(sut.matches('src/foo.ts')).true;
});

it('should not match if the pattern is set to `false`', () => {
const sut = new FileMatcher(false);
expect(sut.matches('src/foo.js')).false;
Expand Down
22 changes: 17 additions & 5 deletions packages/instrumenter/src/disable-type-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,27 @@ const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/;
const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/;
const startingCommentRegex = /(^\s*\/\*.*?\*\/)/gs;

/**
* Disables TypeScript type checking for a single file by inserting `// @ts-nocheck` commands.
* It also does this for *.js files, as they can be type checked by typescript as well.
* Other file types are silently ignored
*
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#-ts-nocheck-in-typescript-files
*/
export async function disableTypeChecks(file: File, options: ParserOptions): Promise<File> {
if (isJSFileWithoutTSDirectives(file)) {
const format = getFormat(file.name);
if (!format) {
// Readme files and stuff don't need disabling.
return file;
}
if (isJSFileWithoutTSDirectives(file, format)) {
// Performance optimization. Only parse the file when it has a change of containing a `// @ts-` directive
return {
...file,
content: prefixWithNoCheck(file.content),
};
}

const parse = createParser(options);
const ast = await parse(file.content, file.name);
switch (ast.format) {
Expand All @@ -30,8 +43,7 @@ export async function disableTypeChecks(file: File, options: ParserOptions): Pro
}
}

function isJSFileWithoutTSDirectives(file: File) {
const format = getFormat(file.name);
function isJSFileWithoutTSDirectives(file: File, format: AstFormat) {
return (format === AstFormat.TS || format === AstFormat.JS) && !tsDirectiveLikeRegEx.test(file.content);
}

Expand All @@ -44,15 +56,15 @@ function prefixWithNoCheck(code: string): string {
// first line has a shebang (#!/usr/bin/env node)
const newLineIndex = code.indexOf('\n');
if (newLineIndex > 0) {
return `${code.substr(0, newLineIndex)}\n// @ts-nocheck\n${code.substr(newLineIndex + 1)}`;
return `${code.substring(0, newLineIndex)}\n// @ts-nocheck\n${code.substring(newLineIndex + 1)}`;
} else {
return code;
}
} else {
// We should leave comments, like `/** @jest-env jsdom */ at the top of the file, see #2569
startingCommentRegex.lastIndex = 0;
const commentMatch = startingCommentRegex.exec(code);
return `${commentMatch?.[1].concat('\n') ?? ''}// @ts-nocheck\n${code.substr(commentMatch?.[1].length ?? 0)}`;
return `${commentMatch?.[1].concat('\n') ?? ''}// @ts-nocheck\n${code.substring(commentMatch?.[1].length ?? 0)}`;
}
}

Expand Down
56 changes: 56 additions & 0 deletions packages/instrumenter/src/parsers/create-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import path from 'path';

import { AstByFormat, AstFormat } from '../syntax/index.js';

import { createParser as createJSParser } from './js-parser.js';
import { parseTS, parseTsx } from './ts-parser.js';
import { parse as htmlParse } from './html-parser.js';
import { ParserOptions } from './parser-options.js';

export function createParser(
parserOptions: ParserOptions
): <T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T | undefined) => Promise<AstByFormat[T]> {
const jsParse = createJSParser(parserOptions);
return async function parse<T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T): Promise<AstByFormat[T]> {
const format = getFormat(fileName, formatOverride);
if (!format) {
const ext = path.extname(fileName).toLowerCase();
throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
}
switch (format) {
case AstFormat.JS:
return jsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Tsx:
return parseTsx(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.TS:
return parseTS(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Html:
return htmlParse(code, fileName, { parse }) as Promise<AstByFormat[T]>;
}
};
}

export function getFormat(fileName: string, override?: AstFormat): AstFormat | undefined {
if (override) {
return override;
} else {
const ext = path.extname(fileName).toLowerCase();
switch (ext) {
case '.js':
case '.jsx':
case '.mjs':
case '.cjs':
return AstFormat.JS;
case '.ts':
return AstFormat.TS;
case '.tsx':
return AstFormat.Tsx;
case '.vue':
case '.html':
case '.htm':
return AstFormat.Html;
default:
return;
}
}
}
53 changes: 2 additions & 51 deletions packages/instrumenter/src/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,5 @@
import path from 'path';

import { AstFormat, AstByFormat } from '../syntax/index.js';

import { createParser as createJSParser } from './js-parser.js';
import { parseTS, parseTsx } from './ts-parser.js';
import { parse as htmlParse } from './html-parser.js';
import { ParserOptions } from './parser-options.js';
import { createParser, getFormat } from './create-parser.js';

export type { ParserOptions };

export function createParser(
parserOptions: ParserOptions
): <T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T | undefined) => Promise<AstByFormat[T]> {
const jsParse = createJSParser(parserOptions);
return function parse<T extends AstFormat = AstFormat>(code: string, fileName: string, formatOverride?: T): Promise<AstByFormat[T]> {
const format = getFormat(fileName, formatOverride);
switch (format) {
case AstFormat.JS:
return jsParse(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Tsx:
return parseTsx(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.TS:
return parseTS(code, fileName) as Promise<AstByFormat[T]>;
case AstFormat.Html:
return htmlParse(code, fileName, { parse }) as Promise<AstByFormat[T]>;
}
};
}

export function getFormat(fileName: string, override?: AstFormat): AstFormat {
if (override) {
return override;
} else {
const ext = path.extname(fileName).toLowerCase();
switch (ext) {
case '.js':
case '.jsx':
case '.mjs':
case '.cjs':
return AstFormat.JS;
case '.ts':
return AstFormat.TS;
case '.tsx':
return AstFormat.Tsx;
case '.vue':
case '.html':
case '.htm':
return AstFormat.Html;
default:
throw new Error(`Unable to parse ${fileName}. No parser registered for ${ext}!`);
}
}
}
export { createParser, getFormat };
9 changes: 9 additions & 0 deletions packages/instrumenter/test/unit/disable-type-checks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { assertions } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';

import { disableTypeChecks, File } from '../../src/index.js';

Expand Down Expand Up @@ -153,4 +154,12 @@ describe(disableTypeChecks.name, () => {
assertions.expectTextFileEqual(actual, { name: 'foo.vue', content: '<template>\n// @ts-expect-error\n</template>' });
});
});

describe('with unsupported AST format', () => {
it('should silently ignore the file', async () => {
const expectedFile = { content: '# Readme', name: 'readme.md', mutate: true };
const actualFile = await disableTypeChecks(expectedFile, { plugins: null });
expect(actualFile).eq(expectedFile);
});
});
});
10 changes: 10 additions & 0 deletions packages/instrumenter/test/unit/parsers/create-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect } from 'chai';

import { createParser } from '../../../src/parsers/index.js';

describe(createParser.name, () => {
it('should throw an error if the file extension is not supported', async () => {
const sut = createParser({ plugins: null });
await expect(sut('# Readme', 'readme.md')).rejectedWith('Unable to parse readme.md. No parser registered for .md!');
});
});

0 comments on commit 3c3d298

Please sign in to comment.