From 8f27af924e73f848e15de24263e8a7c5bb7405a1 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Sun, 30 Aug 2020 21:23:51 +0200 Subject: [PATCH 01/13] feat(typescript): Disable type checking DIsable type checking by inserting `// @ts-nocheck` and removing all other `// @ts-xxx` directives. This is done in the instrumenter, by parsing the input files and scanning the comments. It can be configured with: ```json { "sandbox": { "disableTypeChecking": "**/*.ts" } } ``` You can disable it completely by setting `disableTypeChecking` to `false`. This setting replaces both `sandbox.fileHeaders` and `sandbox.stripComments`. --- packages/api/schema/stryker-core.json | 17 +++- .../core/src/sandbox/create-preprocessor.ts | 9 +- .../disable-type-checking-preprocessor.ts | 35 +++++++ .../src/sandbox/file-header-preprocessor.ts | 27 ------ .../sandbox/strip-comments-preprocessor.ts | 33 ------- .../unit/sandbox/create-preprocessor.spec.ts | 4 +- ...disable-type-checking-preprocessor.spec.ts | 0 .../sandbox/file-header-preprocessor.spec.ts | 96 ------------------- .../strip-comments-preprocessor.spec.ts | 54 ----------- .../instrumenter/src/disable-type-checking.ts | 47 +++++++++ packages/instrumenter/src/index.ts | 1 + .../test/unit/disable-type-checking.spec.ts | 64 +++++++++++++ 12 files changed, 166 insertions(+), 221 deletions(-) create mode 100644 packages/core/src/sandbox/disable-type-checking-preprocessor.ts delete mode 100644 packages/core/src/sandbox/file-header-preprocessor.ts delete mode 100644 packages/core/src/sandbox/strip-comments-preprocessor.ts create mode 100644 packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts delete mode 100644 packages/core/test/unit/sandbox/file-header-preprocessor.spec.ts delete mode 100644 packages/core/test/unit/sandbox/strip-comments-preprocessor.spec.ts create mode 100644 packages/instrumenter/src/disable-type-checking.ts create mode 100644 packages/instrumenter/test/unit/disable-type-checking.spec.ts diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 27da19829e..bed745d363 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -176,7 +176,6 @@ "type": "string" }, "default": { - "**/*+(.js|.ts|.cjs|.mjs)?(x)": "/* eslint-disable */\n// @ts-nocheck\n" } }, "stripComments": { @@ -191,7 +190,21 @@ "type": "string" } ], - "default": "**/*+(.js|.ts|.cjs|.mjs)?(x)" + "default": false + }, + "disableTypeChecking": { + "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 (by design). 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 your sandbox, you can specify a different glob expression or set it to `false` to completely disable this behavior.", + "anyOf": [ + { + "enum": [ + false + ] + }, + { + "type": "string" + } + ], + "default": "**/*+(.js|.ts)?(x)" } } }, diff --git a/packages/core/src/sandbox/create-preprocessor.ts b/packages/core/src/sandbox/create-preprocessor.ts index a3f9fd59ca..94e3c5eff5 100644 --- a/packages/core/src/sandbox/create-preprocessor.ts +++ b/packages/core/src/sandbox/create-preprocessor.ts @@ -1,16 +1,11 @@ import { tokens, Injector, commonTokens, PluginContext } from '@stryker-mutator/api/plugin'; import { TSConfigPreprocessor } from './ts-config-preprocessor'; -import { FileHeaderPreprocessor } from './file-header-preprocessor'; import { FilePreprocessor } from './file-preprocessor'; import { MultiPreprocessor } from './multi-preprocessor'; -import { StripCommentsPreprocessor } from './strip-comments-preprocessor'; +import { DisableTypeCheckingPreprocessor } from './disable-type-checking-preprocessor'; createPreprocessor.inject = tokens(commonTokens.injector); export function createPreprocessor(injector: Injector): FilePreprocessor { - return new MultiPreprocessor([ - injector.injectClass(StripCommentsPreprocessor), - injector.injectClass(TSConfigPreprocessor), - injector.injectClass(FileHeaderPreprocessor), - ]); + return new MultiPreprocessor([injector.injectClass(DisableTypeCheckingPreprocessor), injector.injectClass(TSConfigPreprocessor)]); } diff --git a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts new file mode 100644 index 0000000000..db76d0e2b6 --- /dev/null +++ b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts @@ -0,0 +1,35 @@ +import path = require('path'); + +import minimatch = require('minimatch'); +import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; +import { File, StrykerOptions } from '@stryker-mutator/api/core'; + +import { disableTypeChecking } from '@stryker-mutator/instrumenter'; + +import { FilePreprocessor } from './file-preprocessor'; + +/** + * Disabled type checking by inserting `@ts-nocheck` atop TS/JS files and removing other @ts-xxx directives from comments: + * @see https://github.com/stryker-mutator/stryker/issues/2438 + */ +export class DisableTypeCheckingPreprocessor implements FilePreprocessor { + public static readonly inject = tokens(commonTokens.options); + constructor(private readonly options: StrykerOptions) {} + + public async preprocess(files: File[]): Promise { + if (this.options.sandbox.disableTypeChecking === false) { + return files; + } else { + const pattern = path.resolve(this.options.sandbox.disableTypeChecking); + return Promise.all( + files.map(async (file) => { + if (minimatch(path.resolve(file.name), pattern)) { + return await disableTypeChecking(file, { plugins: this.options.mutator.plugins }); + } else { + return file; + } + }) + ); + } + } +} diff --git a/packages/core/src/sandbox/file-header-preprocessor.ts b/packages/core/src/sandbox/file-header-preprocessor.ts deleted file mode 100644 index 91f2d35c95..0000000000 --- a/packages/core/src/sandbox/file-header-preprocessor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import path = require('path'); - -import { File, StrykerOptions } from '@stryker-mutator/api/core'; -import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; -import minimatch = require('minimatch'); - -import { FilePreprocessor } from './file-preprocessor'; - -/** - * https://github.com/stryker-mutator/stryker/issues/2276 - */ -export class FileHeaderPreprocessor implements FilePreprocessor { - public static readonly inject = tokens(commonTokens.options); - - constructor(private readonly options: StrykerOptions) {} - - public async preprocess(files: File[]): Promise { - return files.map((file) => { - Object.entries(this.options.sandbox.fileHeaders).forEach(([pattern, header]) => { - if (minimatch(path.resolve(file.name), path.resolve(pattern))) { - file = new File(file.name, `${header}${file.textContent}`); - } - }); - return file; - }); - } -} diff --git a/packages/core/src/sandbox/strip-comments-preprocessor.ts b/packages/core/src/sandbox/strip-comments-preprocessor.ts deleted file mode 100644 index c23c8ae3ff..0000000000 --- a/packages/core/src/sandbox/strip-comments-preprocessor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import path = require('path'); - -import { File, StrykerOptions } from '@stryker-mutator/api/core'; -import stripComments = require('strip-comments'); -import minimatch = require('minimatch'); -import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; - -import { FilePreprocessor } from './file-preprocessor'; - -/** - * Strips comments from files to get around issue: 2364. - * @see https://github.com/stryker-mutator/stryker/issues/2364 - */ -export class StripCommentsPreprocessor implements FilePreprocessor { - public static readonly inject = tokens(commonTokens.options); - - constructor(private readonly options: StrykerOptions) {} - - public async preprocess(files: File[]): Promise { - if (this.options.sandbox.stripComments === false) { - return files; - } else { - const pattern = path.resolve(this.options.sandbox.stripComments); - return files.map((file) => { - if (minimatch(path.resolve(file.name), pattern)) { - return new File(file.name, stripComments(file.textContent)); - } else { - return file; - } - }); - } - } -} diff --git a/packages/core/test/unit/sandbox/create-preprocessor.spec.ts b/packages/core/test/unit/sandbox/create-preprocessor.spec.ts index 64839bd876..f8ec5775b2 100644 --- a/packages/core/test/unit/sandbox/create-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/create-preprocessor.spec.ts @@ -19,11 +19,11 @@ describe(createPreprocessor.name, () => { it('should add a header to .ts files', async () => { const output = await sut.preprocess([new File(path.resolve('app.ts'), 'foo.bar()')]); - assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '/* eslint-disable */\n// @ts-nocheck\nfoo.bar()')]); + assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '// @ts-nocheck\nfoo.bar()')]); }); it('should strip // @ts-expect-error (see https://github.com/stryker-mutator/stryker/issues/2364)', async () => { const output = await sut.preprocess([new File(path.resolve('app.ts'), '// @ts-expect-error\nfoo.bar()')]); - assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '/* eslint-disable */\n// @ts-nocheck\n\nfoo.bar()')]); + assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '// @ts-nocheck\n// \nfoo.bar()')]); }); }); diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/test/unit/sandbox/file-header-preprocessor.spec.ts b/packages/core/test/unit/sandbox/file-header-preprocessor.spec.ts deleted file mode 100644 index 96ee2f4d07..0000000000 --- a/packages/core/test/unit/sandbox/file-header-preprocessor.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import path = require('path'); - -import { testInjector, assertions } from '@stryker-mutator/test-helpers'; -import { File } from '@stryker-mutator/api/core'; - -import { FileHeaderPreprocessor } from '../../../src/sandbox/file-header-preprocessor'; - -const EXPECTED_DEFAULT_HEADER = '/* eslint-disable */\n// @ts-nocheck\n'; - -describe(FileHeaderPreprocessor.name, () => { - let sut: FileHeaderPreprocessor; - beforeEach(() => { - sut = testInjector.injector.injectClass(FileHeaderPreprocessor); - }); - - it('should add preprocess any js or friend file by default', async () => { - const inputContent = 'foo.bar()'; - const expectedOutputContent = `${EXPECTED_DEFAULT_HEADER}foo.bar()`; - const input = [ - new File('src/app.js', inputContent), - new File('test/app.spec.ts', inputContent), - new File('src/components/app.jsx', inputContent), - new File('src/components/app.tsx', inputContent), - new File('src/components/app.cjs', inputContent), - new File('src/components/app.mjs', inputContent), - ]; - const output = await sut.preprocess(input); - - assertions.expectTextFilesEqual(output, [ - new File('src/app.js', expectedOutputContent), - new File('test/app.spec.ts', expectedOutputContent), - new File('src/components/app.jsx', expectedOutputContent), - new File('src/components/app.tsx', expectedOutputContent), - new File('src/components/app.cjs', expectedOutputContent), - new File('src/components/app.mjs', expectedOutputContent), - ]); - }); - - it('should also match a full file name', async () => { - // Arrange - const input = [new File(path.resolve('src', 'app.ts'), 'foo.bar()')]; - testInjector.options.sandbox.fileHeaders = { - ['+(src|test)/**/*+(.js|.ts)?(x)']: '// @ts-nocheck\n', - }; - - // Act - const output = await sut.preprocess(input); - - // Assert - assertions.expectTextFilesEqual(output, [new File(path.resolve('src', 'app.ts'), '// @ts-nocheck\nfoo.bar()')]); - }); - - it('should not change unmatched files according to glob expression', async () => { - // Arrange - const input = [ - new File(path.resolve('src', 'app.ts'), 'foo.bar()'), - new File(path.resolve('testResources', 'app.ts'), '// test file example that should be ignored'), - ]; - testInjector.options.sandbox.fileHeaders = { - ['+(src|test)/**/*+(.js|.ts)?(x)']: '// @ts-nocheck\n', - }; - - // Act - const output = await sut.preprocess(input); - - // Assert - assertions.expectTextFilesEqual(output, [ - new File(path.resolve('src', 'app.ts'), '// @ts-nocheck\nfoo.bar()'), - new File(path.resolve('testResources', 'app.ts'), '// test file example that should be ignored'), - ]); - }); - - it('should allow multiple headers', async () => { - // Arrange - const input = [new File('src/app.ts', 'foo.bar()'), new File('src/components/app.component.js', 'baz.qux()')]; - testInjector.options.sandbox.fileHeaders = { - ['**/*.ts']: '// @ts-nocheck\n', - ['**/*.js']: '/* eslint-disable */\n', - }; - - // Act - const output = await sut.preprocess(input); - - // Assert - assertions.expectTextFilesEqual(output, [ - new File('src/app.ts', '// @ts-nocheck\nfoo.bar()'), - new File('src/components/app.component.js', '/* eslint-disable */\nbaz.qux()'), - ]); - }); - - it('should pass-through any other files', async () => { - const input = [new File('README.md', '# Foo')]; - const output = await sut.preprocess(input); - assertions.expectTextFilesEqual(output, [new File('README.md', '# Foo')]); - }); -}); diff --git a/packages/core/test/unit/sandbox/strip-comments-preprocessor.spec.ts b/packages/core/test/unit/sandbox/strip-comments-preprocessor.spec.ts deleted file mode 100644 index fdde9015ba..0000000000 --- a/packages/core/test/unit/sandbox/strip-comments-preprocessor.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import path = require('path'); - -import { testInjector, assertions } from '@stryker-mutator/test-helpers'; -import { File } from '@stryker-mutator/api/core'; - -import { expect } from 'chai'; - -import { StripCommentsPreprocessor } from '../../../src/sandbox/strip-comments-preprocessor'; - -describe(StripCommentsPreprocessor.name, () => { - let sut: StripCommentsPreprocessor; - - beforeEach(() => { - sut = testInjector.injector.injectClass(StripCommentsPreprocessor); - }); - - it('should strip comments', async () => { - const input = [new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();')]; - const output = await sut.preprocess(input); - assertions.expectTextFilesEqual(output, [new File(path.resolve('src/app.ts'), '\nfoo.bar();')]); - }); - - it('should not strip strings with comments in it', async () => { - const input = [new File(path.resolve('src/app.ts'), 'foo.bar("// @ts-expect-error");')]; - const output = await sut.preprocess(input); - assertions.expectTextFilesEqual(output, [new File(path.resolve('src/app.ts'), 'foo.bar("// @ts-expect-error");')]); - }); - - it('should only strip comments in that match the "sandbox.stripComments" glob expression', async () => { - const input = [ - new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();'), - new File(path.resolve('test/app.spec.ts'), '/* @ts-expect-error */\nfoo.bar();'), - new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), - ]; - testInjector.options.sandbox.stripComments = '+(src|test)/**/*.ts'; - const output = await sut.preprocess(input); - assertions.expectTextFilesEqual(output, [ - new File(path.resolve('src/app.ts'), '\nfoo.bar();'), - new File(path.resolve('test/app.spec.ts'), 'foo.bar();'), - new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), - ]); - }); - - it('should not strip comments if "sandbox.stripComments" is set to `false`', async () => { - const input = [ - new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();'), - new File(path.resolve('test/app.spec.ts'), '/* @ts-expect-error */\nfoo.bar();'), - new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), - ]; - testInjector.options.sandbox.stripComments = false; - const output = await sut.preprocess(input); - expect(output).eq(input); - }); -}); diff --git a/packages/instrumenter/src/disable-type-checking.ts b/packages/instrumenter/src/disable-type-checking.ts new file mode 100644 index 0000000000..0cadf347b9 --- /dev/null +++ b/packages/instrumenter/src/disable-type-checking.ts @@ -0,0 +1,47 @@ +import { types } from '@babel/core'; +import { File, Range } from '@stryker-mutator/api/core'; +import { notEmpty } from '@stryker-mutator/util'; + +import { InstrumenterOptions } from './instrumenter-options'; +import { createParser } from './parsers'; +import { AstFormat } from './syntax'; + +export async function disableTypeChecking(file: File, options: InstrumenterOptions) { + const parse = createParser(options); + const ast = await parse(file.textContent, file.name); + switch (ast.format) { + case AstFormat.JS: + case AstFormat.TS: + return new File(file.name, `// @ts-nocheck\n${removeTSDirectives(file.textContent, ast.root.comments)}`); + } + return file; +} + +function removeTSDirectives(text: string, comments: Array | null): string { + const directiveRanges = comments + ?.map(tryParseTSDirective) + .filter(notEmpty) + .sort((a, b) => a[0] - b[0]); + if (directiveRanges) { + let currentIndex = 0; + let pruned = ''; + for (const directiveRange of directiveRanges) { + pruned += text.substring(currentIndex, directiveRange[0]); + currentIndex = directiveRange[1]; + } + pruned += text.substr(currentIndex); + return pruned; + } else { + return text; + } +} + +function tryParseTSDirective(comment: types.CommentBlock | types.CommentLine): Range | undefined { + const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/; + const match = commentDirectiveRegEx.exec(comment.value); + if (match) { + const directiveStartPos = comment.start + match[1].length + 2; // +2 to account for the `//` or `/*` start character + return [directiveStartPos, directiveStartPos + match[2].length + 1]; + } + return undefined; +} diff --git a/packages/instrumenter/src/index.ts b/packages/instrumenter/src/index.ts index 088c6aeeec..6691b99c63 100644 --- a/packages/instrumenter/src/index.ts +++ b/packages/instrumenter/src/index.ts @@ -1,3 +1,4 @@ export * from './instrumenter'; export * from './instrument-result'; export * from './instrumenter-options'; +export * from './disable-type-checking'; diff --git a/packages/instrumenter/test/unit/disable-type-checking.spec.ts b/packages/instrumenter/test/unit/disable-type-checking.spec.ts new file mode 100644 index 0000000000..6b85fd1bc5 --- /dev/null +++ b/packages/instrumenter/test/unit/disable-type-checking.spec.ts @@ -0,0 +1,64 @@ +import { File } from '@stryker-mutator/api/core'; +import { assertions } from '@stryker-mutator/test-helpers'; + +import { disableTypeChecking } from '../../src'; + +describe(disableTypeChecking.name, () => { + describe('with a JS (or friend) file', () => { + it('should remove @ts directive from single line', async () => { + await arrangeActAssert('// @ts-check\nfoo.bar();', '// \nfoo.bar();'); + }); + + it('should remove @ts directive from multiline', async () => { + await arrangeActAssert('/* @ts-expect-error */\nfoo.bar();', '/* */\nfoo.bar();'); + }); + + describe('with string', () => { + it('should not remove @ts directive in double quoted string', async () => { + await arrangeActAssert('foo.bar("/* @ts-expect-error */")'); + }); + + it('should not remove @ts directive in double quoted string after escaped double quote', async () => { + await arrangeActAssert('foo.bar("foo \\"/* @ts-expect-error */")'); + }); + + it('should remove @ts directive after a string', async () => { + await arrangeActAssert('foo.bar("foo \\" bar "/* @ts-expect-error */,\nbaz.qux())', 'foo.bar("foo \\" bar "/* */,\nbaz.qux())'); + }); + + it('should not remove @ts directive in single quoted string', async () => { + await arrangeActAssert("foo.bar('/* @ts-expect-error */')"); + }); + }); + + describe('with regex literals', () => { + it('should not remove @ts directive inside the regex', async () => { + await arrangeActAssert('const regex = / \\/*@ts-check */'); + }); + + it('should remove @ts directives just after a regex', async () => { + await arrangeActAssert('const regex = / \\/*@ts-check */// @ts-expect-error\nfoo.bar()', 'const regex = / \\/*@ts-check */// \nfoo.bar()'); + }); + + it('should allow escape sequence inside the regex', async () => { + await arrangeActAssert('const regex = / \\/ /; // @ts-expect-error', 'const regex = / \\/ /; // '); + }); + + it('should allow `/` inside a character class', async () => { + await arrangeActAssert('const regex = / [/] /; // @ts-check', 'const regex = / [/] /; // '); + }); + }); + + describe('with template strings', () => { + it('should not remove @ts directive inside the literal', async () => { + await arrangeActAssert('const foo = `/*@ts-check */`'); + }); + }); + + async function arrangeActAssert(input: string, expectedOutput = input) { + const inputFile = new File('foo.tsx', input); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.tsx', `// @ts-nocheck\n${expectedOutput}`)); + } + }); +}); From c3da189b88aa3b1f257125b2c6a7d57c8fcd41e2 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 10:55:56 +0200 Subject: [PATCH 02/13] Implement error handling --- packages/core/src/config/OptionsValidator.ts | 6 +- packages/core/src/di/coreTokens.ts | 1 + .../core/src/sandbox/create-preprocessor.ts | 9 ++- .../disable-type-checking-preprocessor.ts | 38 ++++++++-- ...disable-type-checking-preprocessor.spec.ts | 76 +++++++++++++++++++ .../instrumenter/src/disable-type-checking.ts | 46 +++++++++-- packages/instrumenter/src/parsers/index.ts | 2 +- .../instrumenter/src/printers/html-printer.ts | 1 - .../test/integration/printers.it.spec.ts | 2 +- .../test/unit/disable-type-checking.spec.ts | 57 +++++++++++++- .../instrumenter/html-sample.html.out.snap | 1 - .../instrumenter/vue-sample.vue.out.snap | 2 - .../printer/html/hello-world.out.html | 2 - packages/test-helpers/src/factory.ts | 1 + packages/util/src/stringUtils.ts | 43 +++++++++-- packages/util/test/unit/stringUtils.spec.ts | 19 ++++- workspace.code-workspace | 2 +- 17 files changed, 275 insertions(+), 33 deletions(-) diff --git a/packages/core/src/config/OptionsValidator.ts b/packages/core/src/config/OptionsValidator.ts index d07e3f6860..cddec14c41 100644 --- a/packages/core/src/config/OptionsValidator.ts +++ b/packages/core/src/config/OptionsValidator.ts @@ -1,9 +1,9 @@ import os = require('os'); import Ajv = require('ajv'); -import { StrykerOptions, strykerCoreSchema, WarningOptions } from '@stryker-mutator/api/core'; +import { StrykerOptions, strykerCoreSchema } from '@stryker-mutator/api/core'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; -import { noopLogger, propertyPath, deepFreeze } from '@stryker-mutator/util'; +import { noopLogger, propertyPath, deepFreeze, PropertyPathBuilder } from '@stryker-mutator/util'; import { Logger } from '@stryker-mutator/api/logging'; import type { JSONSchema7 } from 'json-schema'; @@ -129,7 +129,7 @@ export function markUnknownOptions(options: StrykerOptions, schema: JSONSchema7, log.warn(`Unknown stryker config option "${unknownPropertyName}".`); }); - const p = `${propertyPath('warnings')}.${propertyPath('unknownOptions')}`; + const p = PropertyPathBuilder.create().prop('warnings').prop('unknownOptions').build(); log.warn(`Possible causes: * Is it a typo on your end? diff --git a/packages/core/src/di/coreTokens.ts b/packages/core/src/di/coreTokens.ts index cf7db38499..6a100c8dd1 100644 --- a/packages/core/src/di/coreTokens.ts +++ b/packages/core/src/di/coreTokens.ts @@ -1,6 +1,7 @@ export const checkerPool = 'checkerPool'; export const checkerFactory = 'checkerFactory'; export const checkerConcurrencyTokens = 'checkerConcurrencyTokens'; +export const disableTypeCheckingHelper = 'disableTypeCheckingHelper'; export const execa = 'execa'; export const cliOptions = 'cliOptions'; export const configReader = 'configReader'; diff --git a/packages/core/src/sandbox/create-preprocessor.ts b/packages/core/src/sandbox/create-preprocessor.ts index 94e3c5eff5..7ae7112aea 100644 --- a/packages/core/src/sandbox/create-preprocessor.ts +++ b/packages/core/src/sandbox/create-preprocessor.ts @@ -1,5 +1,9 @@ import { tokens, Injector, commonTokens, PluginContext } from '@stryker-mutator/api/plugin'; +import { disableTypeChecking } from '@stryker-mutator/instrumenter'; + +import { coreTokens } from '../di'; + import { TSConfigPreprocessor } from './ts-config-preprocessor'; import { FilePreprocessor } from './file-preprocessor'; import { MultiPreprocessor } from './multi-preprocessor'; @@ -7,5 +11,8 @@ import { DisableTypeCheckingPreprocessor } from './disable-type-checking-preproc createPreprocessor.inject = tokens(commonTokens.injector); export function createPreprocessor(injector: Injector): FilePreprocessor { - return new MultiPreprocessor([injector.injectClass(DisableTypeCheckingPreprocessor), injector.injectClass(TSConfigPreprocessor)]); + return new MultiPreprocessor([ + injector.provideValue(coreTokens.disableTypeCheckingHelper, disableTypeChecking).injectClass(DisableTypeCheckingPreprocessor), + injector.injectClass(TSConfigPreprocessor), + ]); } diff --git a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts index db76d0e2b6..a60875dc93 100644 --- a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts +++ b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts @@ -3,8 +3,12 @@ import path = require('path'); import minimatch = require('minimatch'); import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import type { disableTypeChecking } from '@stryker-mutator/instrumenter'; +import { Logger } from '@stryker-mutator/api/logging'; +import { PropertyPathBuilder } from '@stryker-mutator/util'; -import { disableTypeChecking } from '@stryker-mutator/instrumenter'; +import { coreTokens } from '../di'; +import { isWarningEnabled } from '../utils/objectUtils'; import { FilePreprocessor } from './file-preprocessor'; @@ -13,23 +17,47 @@ import { FilePreprocessor } from './file-preprocessor'; * @see https://github.com/stryker-mutator/stryker/issues/2438 */ export class DisableTypeCheckingPreprocessor implements FilePreprocessor { - public static readonly inject = tokens(commonTokens.options); - constructor(private readonly options: StrykerOptions) {} + public static readonly inject = tokens(commonTokens.logger, commonTokens.options, coreTokens.disableTypeCheckingHelper); + constructor(private readonly log: Logger, private readonly options: StrykerOptions, private readonly impl: typeof disableTypeChecking) {} public async preprocess(files: File[]): Promise { if (this.options.sandbox.disableTypeChecking === false) { return files; } else { const pattern = path.resolve(this.options.sandbox.disableTypeChecking); - return Promise.all( + let warningLogged = false; + const outFiles = await Promise.all( files.map(async (file) => { if (minimatch(path.resolve(file.name), pattern)) { - return await disableTypeChecking(file, { plugins: this.options.mutator.plugins }); + try { + return await this.impl(file, { plugins: this.options.mutator.plugins }); + } catch (err) { + if (isWarningEnabled('preprocessorErrors', this.options.warnings)) { + warningLogged = true; + this.log.warn( + `Unable to disable type checking for file "${ + file.name + }". Shouldn't type checking be disabled for this file? Consider configuring a more restrictive "${PropertyPathBuilder.create< + StrykerOptions + >() + .prop('sandbox') + .prop('disableTypeChecking')}" settings (or turn it completely off with \`false\`)`, + err + ); + } + return file; + } } else { return file; } }) ); + if (warningLogged) { + this.log.warn( + `(disable "${PropertyPathBuilder.create().prop('warnings').prop('preprocessorErrors')}" to ignore this warning` + ); + } + return outFiles; } } } diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts index e69de29bb2..67408912fb 100644 --- a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts @@ -0,0 +1,76 @@ +import path = require('path'); + +import { File } from '@stryker-mutator/api/core'; +import { assertions, testInjector } from '@stryker-mutator/test-helpers'; +import sinon = require('sinon'); + +import { expect } from 'chai'; + +import { coreTokens } from '../../../src/di'; +import { DisableTypeCheckingPreprocessor } from '../../../src/sandbox/disable-type-checking-preprocessor'; + +describe.only(DisableTypeCheckingPreprocessor.name, () => { + let sut: DisableTypeCheckingPreprocessor; + let disableTypeCheckingStub: sinon.SinonStub; + + beforeEach(() => { + disableTypeCheckingStub = sinon.stub(); + sut = testInjector.injector + .provideValue(coreTokens.disableTypeCheckingHelper, disableTypeCheckingStub) + .injectClass(DisableTypeCheckingPreprocessor); + }); + + ['.ts', '.tsx', '.js', '.jsx', '.html', '.vue'].forEach((extension) => { + it(`should disable type checking a ${extension} file by default`, async () => { + const fileName = `src/app${extension}`; + const expectedFile = new File(path.resolve(fileName), 'output'); + const inputFile = new File(path.resolve(fileName), 'input'); + const input = [inputFile]; + disableTypeCheckingStub.resolves(expectedFile); + const output = await sut.preprocess(input); + expect(disableTypeCheckingStub).calledWith(inputFile); + assertions.expectTextFilesEqual(output, [expectedFile]); + }); + }); + + it('should be able to override "sandbox.disableTypeChecking" glob pattern', async () => { + testInjector.options.sandbox.disableTypeChecking = 'src/**/*.ts'; + const expectedFile = new File(path.resolve('src/app.ts'), 'output'); + const input = [new File(path.resolve('src/app.ts'), 'input')]; + disableTypeCheckingStub.resolves(expectedFile); + const output = await sut.preprocess(input); + assertions.expectTextFilesEqual(output, [expectedFile]); + }); + + it('should not disable type checking when the "sandbox.disableTypeChecking" glob pattern does not match', async () => { + testInjector.options.sandbox.disableTypeChecking = 'src/**/*.ts'; + const expectedFiles = [new File(path.resolve('test/app.spec.ts'), 'input')]; + disableTypeCheckingStub.resolves(new File('', 'not expected')); + const output = await sut.preprocess(expectedFiles); + assertions.expectTextFilesEqual(output, expectedFiles); + }); + + it('should not disable type checking if "sandbox.disableTypeChecking" is set to `false`', async () => { + const input = [ + new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();'), + new File(path.resolve('test/app.spec.ts'), '/* @ts-expect-error */\nfoo.bar();'), + new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), + ]; + testInjector.options.sandbox.disableTypeChecking = false; + const output = await sut.preprocess(input); + assertions.expectTextFilesEqual(output, input); + }); + + it('should not crash on error, instead log a warning', async () => { + const input = [new File('src/app.ts', 'input')]; + const expectedError = new Error('Expected error for testing'); + disableTypeCheckingStub.rejects(expectedError); + const output = await sut.preprocess(input); + expect(testInjector.logger.warn).calledWithExactly( + 'Unable to disable type checking for file "src/app.ts". Shouldn\'t type checking be disabled for this file? Consider configuring a more restrictive "sandbox.disableTypeChecking" settings (or turn it completely off with `false`)', + expectedError + ); + expect(testInjector.logger.warn).calledWithExactly('(disable "warnings.preprocessorErrors" to ignore this warning'); + assertions.expectTextFilesEqual(output, input); + }); +}); diff --git a/packages/instrumenter/src/disable-type-checking.ts b/packages/instrumenter/src/disable-type-checking.ts index 0cadf347b9..7c21eca94f 100644 --- a/packages/instrumenter/src/disable-type-checking.ts +++ b/packages/instrumenter/src/disable-type-checking.ts @@ -3,18 +3,55 @@ import { File, Range } from '@stryker-mutator/api/core'; import { notEmpty } from '@stryker-mutator/util'; import { InstrumenterOptions } from './instrumenter-options'; -import { createParser } from './parsers'; -import { AstFormat } from './syntax'; +import { createParser, getFormat } from './parsers'; +import { AstFormat, HtmlAst, JSAst, TSAst } from './syntax'; + +const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/; +const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/; export async function disableTypeChecking(file: File, options: InstrumenterOptions) { + if (isJSFileWithoutTSDirectives(file)) { + // Performance optimization. Only parse the file when it has a change of containing a `// @ts-` directive + return new File(file.name, prefixWithNoCheck(file.textContent)); + } const parse = createParser(options); const ast = await parse(file.textContent, file.name); switch (ast.format) { case AstFormat.JS: case AstFormat.TS: - return new File(file.name, `// @ts-nocheck\n${removeTSDirectives(file.textContent, ast.root.comments)}`); + return new File(file.name, disableTypeCheckingInBabelAst(ast)); + case AstFormat.Html: + return new File(file.name, disableTypeCheckingInHtml(ast)); + } +} + +function isJSFileWithoutTSDirectives(file: File) { + const format = getFormat(file.name); + return (format === AstFormat.TS || format === AstFormat.JS) && !tsDirectiveLikeRegEx.test(file.textContent); +} + +function disableTypeCheckingInBabelAst(ast: JSAst | TSAst): string { + return prefixWithNoCheck(removeTSDirectives(ast.rawContent, ast.root.comments)); +} + +function prefixWithNoCheck(code: string): string { + return `// @ts-nocheck\n${code}`; +} + +function disableTypeCheckingInHtml(ast: HtmlAst): string { + const sortedScripts = [...ast.root.scripts].sort((a, b) => a.root.start! - b.root.start!); + let currentIndex = 0; + let html = ''; + for (const script of sortedScripts) { + html += ast.rawContent.substring(currentIndex, script.root.start!); + html += '\n'; + html += '// @ts-nocheck\n'; + html += removeTSDirectives(script.rawContent, script.root.comments); + html += '\n'; + currentIndex = script.root.end!; } - return file; + html += ast.rawContent.substr(currentIndex); + return html; } function removeTSDirectives(text: string, comments: Array | null): string { @@ -37,7 +74,6 @@ function removeTSDirectives(text: string, comments: Array = (ast, context) => { for (const script of sortedScripts) { html += ast.rawContent.substring(currentIndex, script.root.start!); html += '\n'; - html += '// @ts-nocheck\n'; html += context.print(script, context); html += '\n'; currentIndex = script.root.end!; diff --git a/packages/instrumenter/test/integration/printers.it.spec.ts b/packages/instrumenter/test/integration/printers.it.spec.ts index b61a64adf2..b24833f059 100644 --- a/packages/instrumenter/test/integration/printers.it.spec.ts +++ b/packages/instrumenter/test/integration/printers.it.spec.ts @@ -25,7 +25,7 @@ describe('parse and print integration', () => { } }); describe('html', () => { - it('should be able to print html files with multiple script tags and add // @ts-nocheck comments', async () => { + it('should be able to print html files with multiple script tags', async () => { await actArrangeAndAssert('hello-world'); }); diff --git a/packages/instrumenter/test/unit/disable-type-checking.spec.ts b/packages/instrumenter/test/unit/disable-type-checking.spec.ts index 6b85fd1bc5..5dc23d46b9 100644 --- a/packages/instrumenter/test/unit/disable-type-checking.spec.ts +++ b/packages/instrumenter/test/unit/disable-type-checking.spec.ts @@ -1,14 +1,46 @@ import { File } from '@stryker-mutator/api/core'; import { assertions } from '@stryker-mutator/test-helpers'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import * as parsers from '../../src/parsers'; import { disableTypeChecking } from '../../src'; describe(disableTypeChecking.name, () => { - describe('with a JS (or friend) file', () => { + describe('with TS or JS AST format', () => { + it('should prefix the file with `// @ts-nocheck`', async () => { + const inputFile = new File('foo.js', 'foo.bar();'); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.js', '// @ts-nocheck\nfoo.bar();')); + }); + + it('should not even parse the file if "@ts-" can\'t be found anywhere in the file (performance optimization)', async () => { + const createParserSpy = sinon.spy(parsers, 'createParser'); + const inputFile = new File('foo.js', 'foo.bar();'); + await disableTypeChecking(inputFile, { plugins: null }); + expect(createParserSpy).not.called; + }); + + it('should remove @ts directives from a JS file', async () => { + const inputFile = new File('foo.js', '// @ts-check\nfoo.bar();'); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.js', '// @ts-nocheck\n// \nfoo.bar();')); + }); + + it('should remove @ts directives from a TS file', async () => { + const inputFile = new File('foo.ts', '// @ts-check\nfoo.bar();'); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.ts', '// @ts-nocheck\n// \nfoo.bar();')); + }); + it('should remove @ts directive from single line', async () => { await arrangeActAssert('// @ts-check\nfoo.bar();', '// \nfoo.bar();'); }); + it('should not remove @ts comments which occur later on the comment line (since then they are not considered a directive)', async () => { + await arrangeActAssert('// this should be ignored: @ts-expect-error\nfoo.bar();'); + }); + it('should remove @ts directive from multiline', async () => { await arrangeActAssert('/* @ts-expect-error */\nfoo.bar();', '/* */\nfoo.bar();'); }); @@ -61,4 +93,27 @@ describe(disableTypeChecking.name, () => { assertions.expectTextFileEqual(actual, new File('foo.tsx', `// @ts-nocheck\n${expectedOutput}`)); } }); + + describe('with HTML ast format', () => { + it('should prefix the script tags with `// @ts-nocheck`', async () => { + const inputFile = new File('foo.vue', ''); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.vue', '')); + }); + + it('should remove `// @ts` directives from script tags', async () => { + const inputFile = new File('foo.html', ''); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual( + actual, + new File('foo.html', '') + ); + }); + + it('should not remove `// @ts` from the html itself', async () => { + const inputFile = new File('foo.vue', ''); + const actual = await disableTypeChecking(inputFile, { plugins: null }); + assertions.expectTextFileEqual(actual, new File('foo.vue', '')); + }); + }); }); diff --git a/packages/instrumenter/testResources/instrumenter/html-sample.html.out.snap b/packages/instrumenter/testResources/instrumenter/html-sample.html.out.snap index fd1b9d3400..dd0ebdd788 100644 --- a/packages/instrumenter/testResources/instrumenter/html-sample.html.out.snap +++ b/packages/instrumenter/testResources/instrumenter/html-sample.html.out.snap @@ -12,7 +12,6 @@ exports[`instrumenter integration should be able to instrument html 1`] = ` diff --git a/packages/test-helpers/src/factory.ts b/packages/test-helpers/src/factory.ts index 0fac521916..54b7960113 100644 --- a/packages/test-helpers/src/factory.ts +++ b/packages/test-helpers/src/factory.ts @@ -75,6 +75,7 @@ export function pluginResolver(): sinon.SinonStubbedInstance { export const warningOptions = factoryMethod(() => ({ unknownOptions: true, + preprocessorErrors: true, })); export const killedMutantResult = factoryMethod(() => ({ diff --git a/packages/util/src/stringUtils.ts b/packages/util/src/stringUtils.ts index 7c8d10deea..31d57d553d 100644 --- a/packages/util/src/stringUtils.ts +++ b/packages/util/src/stringUtils.ts @@ -1,4 +1,3 @@ -import { notEmpty } from './notEmpty'; import { KnownKeys } from './KnownKeys'; /** @@ -10,12 +9,44 @@ export function normalizeWhitespaces(str: string) { } /** - * Given a base type, allows type safe access to the name (or deep path) of a property. - * @param prop The first property key - * @param prop2 The optional second property key. Add prop3, prop4, etc to this method signature when needed. + * Given a base type, allows type safe access to the name of a property. + * @param prop The property name */ -export function propertyPath(prop: keyof Pick>, prop2?: keyof Pick>[typeof prop]): string { - return [prop, prop2].filter(notEmpty).join('.'); +export function propertyPath(prop: keyof Pick>): string { + return prop.toString(); +} + +/** + * A helper class to allow you to get type safe access to the name of a deep property of `T` + * @example + * ```ts + * PropertyPathBuilder('warnings').prop('unknownOptions').build() + * ``` + */ +export class PropertyPathBuilder { + constructor(private readonly pathSoFar: string[]) {} + + public prop>(prop: TProp) { + return new PropertyPathBuilder>[TProp]>([...this.pathSoFar, prop.toString()]); + } + + /** + * Build the (deep) path to the property name + */ + public build(): string { + return this.pathSoFar.join('.'); + } + + /** + * Creates a new `PropertyPathBuilder` for type T + */ + public static create() { + return new PropertyPathBuilder([]); + } + + public toString(): string { + return this.build(); + } } /** diff --git a/packages/util/test/unit/stringUtils.spec.ts b/packages/util/test/unit/stringUtils.spec.ts index 802014c10c..1b29eeec18 100644 --- a/packages/util/test/unit/stringUtils.spec.ts +++ b/packages/util/test/unit/stringUtils.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { normalizeWhitespaces, propertyPath, escapeRegExpLiteral, escapeRegExp } from '../../src'; +import { normalizeWhitespaces, propertyPath, escapeRegExpLiteral, escapeRegExp, PropertyPathBuilder } from '../../src'; describe('stringUtils', () => { describe(normalizeWhitespaces.name, () => { @@ -17,15 +17,28 @@ describe('stringUtils', () => { }); }); - describe(propertyPath.name, () => { + describe(PropertyPathBuilder.name, () => { interface Foo { bar: { baz: string; }; + qux: { + quux: string; + }; + } + + it('should be able to point to a path', () => { + expect(PropertyPathBuilder.create().prop('bar').prop('baz').build()).eq('bar.baz'); + }); + }); + + describe(propertyPath.name, () => { + interface Foo { + bar: string; } it('should be able to point to a path', () => { - expect(propertyPath('bar', 'baz')).eq('bar.baz'); + expect(propertyPath('bar')).eq('bar'); }); }); diff --git a/workspace.code-workspace b/workspace.code-workspace index 3698afcded..23695979f8 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -90,7 +90,7 @@ } ], "settings": { - "typescript.tsdk": "parent\\node_modules\\typescript\\lib", + "typescript.tsdk": "parent/node_modules/typescript/lib", "files.exclude": { ".git": true, ".tscache": true, From 838afc74b7b269a26bf26790c558b34b48778c6e Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 11:18:30 +0200 Subject: [PATCH 03/13] Simplify default for `disableTypeChecking` --- packages/api/schema/stryker-core.json | 33 ++++++--------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index bed745d363..50543cc95f 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -168,32 +168,8 @@ "type": "object", "additionalProperties": false, "properties": { - "fileHeaders": { - "type": "object", - "title": "SandboxFileHeaders", - "description": "Configure additional headers to be added to files inside your sandbox. These headers will be added after Stryker has instrumented your code with mutants, but before a test runner or build command is executed. This is used to ignore typescript compile errors and eslint warnings that might have been added in the process of instrumenting your code with mutants. The default setting should work for most use cases.", - "additionalProperties": { - "type": "string" - }, - "default": { - } - }, - "stripComments": { - "description": "Configure files to be stripped of comments (either single line with `//` or multi line with `/**/`. These comments will be stripped after Stryker has instrumented your code with mutants, but before a test runner or build command is executed. This is used to remove any lingering `// @ts-check` or `// @ts-expect-error` comments that interfere with typescript compilation. The default setting allows comments to be stripped from all JavaScript and friend files in your sandbox, you can specify a different glob expression or set it to `false` to completely disable this behavior.", - "anyOf": [ - { - "enum": [ - false - ] - }, - { - "type": "string" - } - ], - "default": false - }, "disableTypeChecking": { - "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 (by design). 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 your sandbox, you can specify a different glob expression or set it to `false` to completely disable this behavior.", + "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 your sandbox, you can specify a different glob expression or set it to `false` to completely disable this behavior.", "anyOf": [ { "enum": [ @@ -204,7 +180,7 @@ "type": "string" } ], - "default": "**/*+(.js|.ts)?(x)" + "default": "**/*.{js,ts,jsx,tsx,html,vue}" } } }, @@ -248,6 +224,11 @@ "description": "decide whether or not to log warnings when additional stryker options are configured", "type": "boolean", "default": true + }, + "preprocessorErrors": { + "description": "decide whether or not to log warnings when a preprocessor warning occurs. For example, when the disabling of type errors fails.", + "type": "boolean", + "default": true } } } From d5d28284f3f0f21e112a9b6df674d4fd25b25a4d Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 11:28:38 +0200 Subject: [PATCH 04/13] fix compile errors --- packages/mocha-runner/src/MochaAdapter.ts | 9 ++++----- packages/mocha-runner/src/MochaOptionsLoader.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/mocha-runner/src/MochaAdapter.ts b/packages/mocha-runner/src/MochaAdapter.ts index 0872078e0e..856d1a7f61 100644 --- a/packages/mocha-runner/src/MochaAdapter.ts +++ b/packages/mocha-runner/src/MochaAdapter.ts @@ -3,7 +3,7 @@ import fs = require('fs'); import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; import { Logger } from '@stryker-mutator/api/logging'; -import { propertyPath } from '@stryker-mutator/util'; +import { PropertyPathBuilder } from '@stryker-mutator/util'; import { MochaOptions, MochaRunnerOptions } from '../src-generated/mocha-runner-options'; @@ -81,10 +81,9 @@ export class MochaAdapter { globPatterns, null, 2 - )}). Please specify the files (glob patterns) containing your tests in ${propertyPath( - 'mochaOptions', - 'spec' - )} in your config file.` + )}). Please specify the files (glob patterns) containing your tests in ${PropertyPathBuilder.create() + .prop('mochaOptions') + .prop('spec')} in your config file.` ); } return [...fileNames]; diff --git a/packages/mocha-runner/src/MochaOptionsLoader.ts b/packages/mocha-runner/src/MochaOptionsLoader.ts index d2f8e0d57a..40724fb615 100644 --- a/packages/mocha-runner/src/MochaOptionsLoader.ts +++ b/packages/mocha-runner/src/MochaOptionsLoader.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; -import { propertyPath } from '@stryker-mutator/util'; +import { PropertyPathBuilder } from '@stryker-mutator/util'; import { MochaOptions, MochaRunnerOptions } from '../src-generated/mocha-runner-options'; @@ -76,7 +76,7 @@ export default class MochaOptionsLoader { } else { this.log.debug( 'No mocha opts file found, not loading additional mocha options (%s was not defined).', - propertyPath('mochaOptions', 'opts') + PropertyPathBuilder.create().prop('mochaOptions').prop('opts') ); return {}; } From 016bb11acf762c8b0e1c9d03e5ef32bf5dfb3957 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 11:43:36 +0200 Subject: [PATCH 05/13] =?UTF-8?q?Remove=20`only`=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/sandbox/disable-type-checking-preprocessor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts index 67408912fb..d6bff17f89 100644 --- a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts @@ -9,7 +9,7 @@ import { expect } from 'chai'; import { coreTokens } from '../../../src/di'; import { DisableTypeCheckingPreprocessor } from '../../../src/sandbox/disable-type-checking-preprocessor'; -describe.only(DisableTypeCheckingPreprocessor.name, () => { +describe(DisableTypeCheckingPreprocessor.name, () => { let sut: DisableTypeCheckingPreprocessor; let disableTypeCheckingStub: sinon.SinonStub; From 7cf9bab57cae435ae2277554a5ebd5ed9caf2723 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 11:44:54 +0200 Subject: [PATCH 06/13] Update express config in perf test --- perf/config/express/stryker.conf.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/perf/config/express/stryker.conf.json b/perf/config/express/stryker.conf.json index 378d855fd5..bbc46e84a7 100644 --- a/perf/config/express/stryker.conf.json +++ b/perf/config/express/stryker.conf.json @@ -1,8 +1,5 @@ { "$schema": "../../../packages/core/schema/stryker-schema.json", "testRunner": "mocha", - "coverageAnalysis": "perTest", - "sandbox": { - "stripComments": false - } + "coverageAnalysis": "perTest" } From 3beec1fd8d85f082c3301c1f99cd4627e0a52a19 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 11:53:43 +0200 Subject: [PATCH 07/13] fix test --- packages/mocha-runner/src/MochaOptionsLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mocha-runner/src/MochaOptionsLoader.ts b/packages/mocha-runner/src/MochaOptionsLoader.ts index 40724fb615..6ea0165aa2 100644 --- a/packages/mocha-runner/src/MochaOptionsLoader.ts +++ b/packages/mocha-runner/src/MochaOptionsLoader.ts @@ -76,7 +76,7 @@ export default class MochaOptionsLoader { } else { this.log.debug( 'No mocha opts file found, not loading additional mocha options (%s was not defined).', - PropertyPathBuilder.create().prop('mochaOptions').prop('opts') + PropertyPathBuilder.create().prop('mochaOptions').prop('opts').build() ); return {}; } From f88d1145f7ddb87b31422b1a0a653f195aefc84d Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 13:45:37 +0200 Subject: [PATCH 08/13] small refactor in disable type checking --- packages/instrumenter/src/disable-type-checking.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/instrumenter/src/disable-type-checking.ts b/packages/instrumenter/src/disable-type-checking.ts index 7c21eca94f..3490b0b414 100644 --- a/packages/instrumenter/src/disable-type-checking.ts +++ b/packages/instrumenter/src/disable-type-checking.ts @@ -45,8 +45,7 @@ function disableTypeCheckingInHtml(ast: HtmlAst): string { for (const script of sortedScripts) { html += ast.rawContent.substring(currentIndex, script.root.start!); html += '\n'; - html += '// @ts-nocheck\n'; - html += removeTSDirectives(script.rawContent, script.root.comments); + html += prefixWithNoCheck(removeTSDirectives(script.rawContent, script.root.comments)); html += '\n'; currentIndex = script.root.end!; } From 5fb786b8bdb017907740b1c772a7e681f0b0b992 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Aug 2020 13:45:57 +0200 Subject: [PATCH 09/13] docs: improve warning option doc --- packages/api/schema/stryker-core.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 50543cc95f..cf38a6ea6a 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -226,7 +226,7 @@ "default": true }, "preprocessorErrors": { - "description": "decide whether or not to log warnings when a preprocessor warning occurs. For example, when the disabling of type errors fails.", + "description": "decide whether or not to log warnings when a preprocessor error occurs. For example, when the disabling of type errors fails.", "type": "boolean", "default": true } From 06704155e25f04a822fb89df90e8792530b789a3 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Sep 2020 20:34:43 +0200 Subject: [PATCH 10/13] Change remove "sandbox" option and change the default for "disableTypeChecking" --- packages/api/schema/stryker-core.json | 37 +++++++------------ .../disable-type-checking-preprocessor.ts | 14 +++---- .../unit/sandbox/create-preprocessor.spec.ts | 10 ++--- ...disable-type-checking-preprocessor.spec.ts | 14 +++---- perf/config/express/package.json | 1 + 5 files changed, 32 insertions(+), 44 deletions(-) diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index cf38a6ea6a..bc50dc4b6a 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -164,26 +164,6 @@ "minimum": 0, "maximum": 100 }, - "sandboxOptions": { - "type": "object", - "additionalProperties": false, - "properties": { - "disableTypeChecking": { - "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 your sandbox, you can specify a different glob expression or set it to `false` to completely disable this behavior.", - "anyOf": [ - { - "enum": [ - false - ] - }, - { - "type": "string" - } - ], - "default": "**/*.{js,ts,jsx,tsx,html,vue}" - } - } - }, "mutatorDescriptor": { "type": "object", "additionalProperties": false, @@ -358,10 +338,19 @@ "description": "The options for the html reporter", "$ref": "#/definitions/htmlReporterOptions" }, - "sandbox": { - "description": "Configure how the files in the sandbox behave. The sandbox is a copy of your source code where Stryker does mutation testing.", - "$ref": "#/definitions/sandboxOptions", - "default": {} + "disableTypeChecking": { + "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.", + "anyOf": [ + { + "enum": [ + false + ] + }, + { + "type": "string" + } + ], + "default": "{test,src,lib}/**/*.{js,ts,jsx,tsx,html,vue}" }, "symlinkNodeModules": { "description": "The 'symlinkNodeModules' value indicates whether Stryker should create a symbolic link to your current node_modules directory in the sandbox directories. This makes running your tests by Stryker behave more like your would run the tests yourself in your project directory. Only disable this setting if you really know what you are doing.", diff --git a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts index a60875dc93..916df9ccd9 100644 --- a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts +++ b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts @@ -5,7 +5,7 @@ import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { File, StrykerOptions } from '@stryker-mutator/api/core'; import type { disableTypeChecking } from '@stryker-mutator/instrumenter'; import { Logger } from '@stryker-mutator/api/logging'; -import { PropertyPathBuilder } from '@stryker-mutator/util'; +import { propertyPath, PropertyPathBuilder } from '@stryker-mutator/util'; import { coreTokens } from '../di'; import { isWarningEnabled } from '../utils/objectUtils'; @@ -21,10 +21,10 @@ export class DisableTypeCheckingPreprocessor implements FilePreprocessor { constructor(private readonly log: Logger, private readonly options: StrykerOptions, private readonly impl: typeof disableTypeChecking) {} public async preprocess(files: File[]): Promise { - if (this.options.sandbox.disableTypeChecking === false) { + if (this.options.disableTypeChecking === false) { return files; } else { - const pattern = path.resolve(this.options.sandbox.disableTypeChecking); + const pattern = path.resolve(this.options.disableTypeChecking); let warningLogged = false; const outFiles = await Promise.all( files.map(async (file) => { @@ -37,11 +37,9 @@ export class DisableTypeCheckingPreprocessor implements FilePreprocessor { this.log.warn( `Unable to disable type checking for file "${ file.name - }". Shouldn't type checking be disabled for this file? Consider configuring a more restrictive "${PropertyPathBuilder.create< - StrykerOptions - >() - .prop('sandbox') - .prop('disableTypeChecking')}" settings (or turn it completely off with \`false\`)`, + }". Shouldn't type checking be disabled for this file? Consider configuring a more restrictive "${propertyPath( + 'disableTypeChecking' + )}" settings (or turn it completely off with \`false\`)`, err ); } diff --git a/packages/core/test/unit/sandbox/create-preprocessor.spec.ts b/packages/core/test/unit/sandbox/create-preprocessor.spec.ts index f8ec5775b2..2cd3b13a2d 100644 --- a/packages/core/test/unit/sandbox/create-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/create-preprocessor.spec.ts @@ -17,13 +17,13 @@ describe(createPreprocessor.name, () => { assertions.expectTextFilesEqual(output, [new File(path.resolve('tsconfig.json'), '{\n "extends": "../../../tsconfig.settings.json"\n}')]); }); - it('should add a header to .ts files', async () => { - const output = await sut.preprocess([new File(path.resolve('app.ts'), 'foo.bar()')]); - assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '// @ts-nocheck\nfoo.bar()')]); + it('should disable type checking for .ts files', async () => { + const output = await sut.preprocess([new File(path.resolve('src/app.ts'), 'foo.bar()')]); + assertions.expectTextFilesEqual(output, [new File(path.resolve('src/app.ts'), '// @ts-nocheck\nfoo.bar()')]); }); it('should strip // @ts-expect-error (see https://github.com/stryker-mutator/stryker/issues/2364)', async () => { - const output = await sut.preprocess([new File(path.resolve('app.ts'), '// @ts-expect-error\nfoo.bar()')]); - assertions.expectTextFilesEqual(output, [new File(path.resolve('app.ts'), '// @ts-nocheck\n// \nfoo.bar()')]); + const output = await sut.preprocess([new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar()')]); + assertions.expectTextFilesEqual(output, [new File(path.resolve('src/app.ts'), '// @ts-nocheck\n// \nfoo.bar()')]); }); }); diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts index d6bff17f89..016480d8ff 100644 --- a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts @@ -33,8 +33,8 @@ describe(DisableTypeCheckingPreprocessor.name, () => { }); }); - it('should be able to override "sandbox.disableTypeChecking" glob pattern', async () => { - testInjector.options.sandbox.disableTypeChecking = 'src/**/*.ts'; + it('should be able to override "disableTypeChecking" glob pattern', async () => { + testInjector.options.disableTypeChecking = 'src/**/*.ts'; const expectedFile = new File(path.resolve('src/app.ts'), 'output'); const input = [new File(path.resolve('src/app.ts'), 'input')]; disableTypeCheckingStub.resolves(expectedFile); @@ -42,21 +42,21 @@ describe(DisableTypeCheckingPreprocessor.name, () => { assertions.expectTextFilesEqual(output, [expectedFile]); }); - it('should not disable type checking when the "sandbox.disableTypeChecking" glob pattern does not match', async () => { - testInjector.options.sandbox.disableTypeChecking = 'src/**/*.ts'; + it('should not disable type checking when the "disableTypeChecking" glob pattern does not match', async () => { + testInjector.options.disableTypeChecking = 'src/**/*.ts'; const expectedFiles = [new File(path.resolve('test/app.spec.ts'), 'input')]; disableTypeCheckingStub.resolves(new File('', 'not expected')); const output = await sut.preprocess(expectedFiles); assertions.expectTextFilesEqual(output, expectedFiles); }); - it('should not disable type checking if "sandbox.disableTypeChecking" is set to `false`', async () => { + it('should not disable type checking if "disableTypeChecking" is set to `false`', async () => { const input = [ new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();'), new File(path.resolve('test/app.spec.ts'), '/* @ts-expect-error */\nfoo.bar();'), new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), ]; - testInjector.options.sandbox.disableTypeChecking = false; + testInjector.options.disableTypeChecking = false; const output = await sut.preprocess(input); assertions.expectTextFilesEqual(output, input); }); @@ -67,7 +67,7 @@ describe(DisableTypeCheckingPreprocessor.name, () => { disableTypeCheckingStub.rejects(expectedError); const output = await sut.preprocess(input); expect(testInjector.logger.warn).calledWithExactly( - 'Unable to disable type checking for file "src/app.ts". Shouldn\'t type checking be disabled for this file? Consider configuring a more restrictive "sandbox.disableTypeChecking" settings (or turn it completely off with `false`)', + 'Unable to disable type checking for file "src/app.ts". Shouldn\'t type checking be disabled for this file? Consider configuring a more restrictive "disableTypeChecking" settings (or turn it completely off with `false`)', expectedError ); expect(testInjector.logger.warn).calledWithExactly('(disable "warnings.preprocessorErrors" to ignore this warning'); diff --git a/perf/config/express/package.json b/perf/config/express/package.json index 4da6715f9c..586e5d1b4a 100644 --- a/perf/config/express/package.json +++ b/perf/config/express/package.json @@ -1,6 +1,7 @@ { "localDependencies": { "@stryker-mutator/core": "../../../packages/core", + "@stryker-mutator/instrumenter": "../../../packages/instrumenter", "@stryker-mutator/api": "../../../packages/api", "@stryker-mutator/mocha-runner": "../../../packages/mocha-runner", "@stryker-mutator/util": "../../../packages/util" From c37faff83ee11b8b523835fcf9705e7115aa3fa8 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Sep 2020 21:06:14 +0200 Subject: [PATCH 11/13] Rename `disableTypeChecking` -> `disableTypeChecks` --- packages/api/schema/stryker-core.json | 2 +- .../sandbox/disable-type-checking-preprocessor.ts | 6 +++--- .../disable-type-checking-preprocessor.spec.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index bc50dc4b6a..591e32a8c8 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -338,7 +338,7 @@ "description": "The options for the html reporter", "$ref": "#/definitions/htmlReporterOptions" }, - "disableTypeChecking": { + "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.", "anyOf": [ { diff --git a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts index 916df9ccd9..82e0f1c793 100644 --- a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts +++ b/packages/core/src/sandbox/disable-type-checking-preprocessor.ts @@ -21,10 +21,10 @@ export class DisableTypeCheckingPreprocessor implements FilePreprocessor { constructor(private readonly log: Logger, private readonly options: StrykerOptions, private readonly impl: typeof disableTypeChecking) {} public async preprocess(files: File[]): Promise { - if (this.options.disableTypeChecking === false) { + if (this.options.disableTypeChecks === false) { return files; } else { - const pattern = path.resolve(this.options.disableTypeChecking); + const pattern = path.resolve(this.options.disableTypeChecks); let warningLogged = false; const outFiles = await Promise.all( files.map(async (file) => { @@ -38,7 +38,7 @@ export class DisableTypeCheckingPreprocessor implements FilePreprocessor { `Unable to disable type checking for file "${ file.name }". Shouldn't type checking be disabled for this file? Consider configuring a more restrictive "${propertyPath( - 'disableTypeChecking' + 'disableTypeChecks' )}" settings (or turn it completely off with \`false\`)`, err ); diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts index 016480d8ff..f5e9e43a6e 100644 --- a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts @@ -33,8 +33,8 @@ describe(DisableTypeCheckingPreprocessor.name, () => { }); }); - it('should be able to override "disableTypeChecking" glob pattern', async () => { - testInjector.options.disableTypeChecking = 'src/**/*.ts'; + it('should be able to override "disableTypeChecks" glob pattern', async () => { + testInjector.options.disableTypeChecks = 'src/**/*.ts'; const expectedFile = new File(path.resolve('src/app.ts'), 'output'); const input = [new File(path.resolve('src/app.ts'), 'input')]; disableTypeCheckingStub.resolves(expectedFile); @@ -42,21 +42,21 @@ describe(DisableTypeCheckingPreprocessor.name, () => { assertions.expectTextFilesEqual(output, [expectedFile]); }); - it('should not disable type checking when the "disableTypeChecking" glob pattern does not match', async () => { - testInjector.options.disableTypeChecking = 'src/**/*.ts'; + it('should not disable type checking when the "disableTypeChecks" glob pattern does not match', async () => { + testInjector.options.disableTypeChecks = 'src/**/*.ts'; const expectedFiles = [new File(path.resolve('test/app.spec.ts'), 'input')]; disableTypeCheckingStub.resolves(new File('', 'not expected')); const output = await sut.preprocess(expectedFiles); assertions.expectTextFilesEqual(output, expectedFiles); }); - it('should not disable type checking if "disableTypeChecking" is set to `false`', async () => { + it('should not disable type checking if "disableTypeChecks" is set to `false`', async () => { const input = [ new File(path.resolve('src/app.ts'), '// @ts-expect-error\nfoo.bar();'), new File(path.resolve('test/app.spec.ts'), '/* @ts-expect-error */\nfoo.bar();'), new File(path.resolve('testResources/project/app.ts'), '/* @ts-expect-error */\nfoo.bar();'), ]; - testInjector.options.disableTypeChecking = false; + testInjector.options.disableTypeChecks = false; const output = await sut.preprocess(input); assertions.expectTextFilesEqual(output, input); }); @@ -67,7 +67,7 @@ describe(DisableTypeCheckingPreprocessor.name, () => { disableTypeCheckingStub.rejects(expectedError); const output = await sut.preprocess(input); expect(testInjector.logger.warn).calledWithExactly( - 'Unable to disable type checking for file "src/app.ts". Shouldn\'t type checking be disabled for this file? Consider configuring a more restrictive "disableTypeChecking" settings (or turn it completely off with `false`)', + 'Unable to disable type checking for file "src/app.ts". Shouldn\'t type checking be disabled for this file? Consider configuring a more restrictive "disableTypeChecks" settings (or turn it completely off with `false`)', expectedError ); expect(testInjector.logger.warn).calledWithExactly('(disable "warnings.preprocessorErrors" to ignore this warning'); From 210d82cd4ba8cadb1637115f7200d773d5176e08 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Sep 2020 21:20:35 +0200 Subject: [PATCH 12/13] rename `DisableTypeChecking` -> `DisableTypeChecks` anywhere in code --- packages/core/src/di/coreTokens.ts | 2 +- .../core/src/sandbox/create-preprocessor.ts | 6 +-- ...ts => disable-type-checks-preprocessor.ts} | 8 ++-- ... disable-type-checks-preprocessor.spec.ts} | 10 ++--- ...ype-checking.ts => disable-type-checks.ts} | 2 +- packages/instrumenter/src/index.ts | 2 +- .../disable-type-checks.it.spec.ts | 40 +++++++++++++++++++ ...ng.spec.ts => disable-type-checks.spec.ts} | 20 +++++----- 8 files changed, 64 insertions(+), 26 deletions(-) rename packages/core/src/sandbox/{disable-type-checking-preprocessor.ts => disable-type-checks-preprocessor.ts} (91%) rename packages/core/test/unit/sandbox/{disable-type-checking-preprocessor.spec.ts => disable-type-checks-preprocessor.spec.ts} (89%) rename packages/instrumenter/src/{disable-type-checking.ts => disable-type-checks.ts} (97%) create mode 100644 packages/instrumenter/test/integration/disable-type-checks.it.spec.ts rename packages/instrumenter/test/unit/{disable-type-checking.spec.ts => disable-type-checks.spec.ts} (87%) diff --git a/packages/core/src/di/coreTokens.ts b/packages/core/src/di/coreTokens.ts index 6a100c8dd1..005d2b9277 100644 --- a/packages/core/src/di/coreTokens.ts +++ b/packages/core/src/di/coreTokens.ts @@ -1,7 +1,7 @@ export const checkerPool = 'checkerPool'; export const checkerFactory = 'checkerFactory'; export const checkerConcurrencyTokens = 'checkerConcurrencyTokens'; -export const disableTypeCheckingHelper = 'disableTypeCheckingHelper'; +export const disableTypeChecksHelper = 'disableTypeChecksHelper'; export const execa = 'execa'; export const cliOptions = 'cliOptions'; export const configReader = 'configReader'; diff --git a/packages/core/src/sandbox/create-preprocessor.ts b/packages/core/src/sandbox/create-preprocessor.ts index 7ae7112aea..dd553480c9 100644 --- a/packages/core/src/sandbox/create-preprocessor.ts +++ b/packages/core/src/sandbox/create-preprocessor.ts @@ -1,18 +1,18 @@ import { tokens, Injector, commonTokens, PluginContext } from '@stryker-mutator/api/plugin'; -import { disableTypeChecking } from '@stryker-mutator/instrumenter'; +import { disableTypeChecks } from '@stryker-mutator/instrumenter'; import { coreTokens } from '../di'; import { TSConfigPreprocessor } from './ts-config-preprocessor'; import { FilePreprocessor } from './file-preprocessor'; import { MultiPreprocessor } from './multi-preprocessor'; -import { DisableTypeCheckingPreprocessor } from './disable-type-checking-preprocessor'; +import { DisableTypeChecksPreprocessor } from './disable-type-checks-preprocessor'; createPreprocessor.inject = tokens(commonTokens.injector); export function createPreprocessor(injector: Injector): FilePreprocessor { return new MultiPreprocessor([ - injector.provideValue(coreTokens.disableTypeCheckingHelper, disableTypeChecking).injectClass(DisableTypeCheckingPreprocessor), + injector.provideValue(coreTokens.disableTypeChecksHelper, disableTypeChecks).injectClass(DisableTypeChecksPreprocessor), injector.injectClass(TSConfigPreprocessor), ]); } diff --git a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts b/packages/core/src/sandbox/disable-type-checks-preprocessor.ts similarity index 91% rename from packages/core/src/sandbox/disable-type-checking-preprocessor.ts rename to packages/core/src/sandbox/disable-type-checks-preprocessor.ts index 82e0f1c793..15438f20e1 100644 --- a/packages/core/src/sandbox/disable-type-checking-preprocessor.ts +++ b/packages/core/src/sandbox/disable-type-checks-preprocessor.ts @@ -3,7 +3,7 @@ import path = require('path'); import minimatch = require('minimatch'); import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { File, StrykerOptions } from '@stryker-mutator/api/core'; -import type { disableTypeChecking } from '@stryker-mutator/instrumenter'; +import type { disableTypeChecks } from '@stryker-mutator/instrumenter'; import { Logger } from '@stryker-mutator/api/logging'; import { propertyPath, PropertyPathBuilder } from '@stryker-mutator/util'; @@ -16,9 +16,9 @@ import { FilePreprocessor } from './file-preprocessor'; * Disabled type checking by inserting `@ts-nocheck` atop TS/JS files and removing other @ts-xxx directives from comments: * @see https://github.com/stryker-mutator/stryker/issues/2438 */ -export class DisableTypeCheckingPreprocessor implements FilePreprocessor { - public static readonly inject = tokens(commonTokens.logger, commonTokens.options, coreTokens.disableTypeCheckingHelper); - constructor(private readonly log: Logger, private readonly options: StrykerOptions, private readonly impl: typeof disableTypeChecking) {} +export class DisableTypeChecksPreprocessor implements FilePreprocessor { + public static readonly inject = tokens(commonTokens.logger, commonTokens.options, coreTokens.disableTypeChecksHelper); + constructor(private readonly log: Logger, private readonly options: StrykerOptions, private readonly impl: typeof disableTypeChecks) {} public async preprocess(files: File[]): Promise { if (this.options.disableTypeChecks === false) { diff --git a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts b/packages/core/test/unit/sandbox/disable-type-checks-preprocessor.spec.ts similarity index 89% rename from packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts rename to packages/core/test/unit/sandbox/disable-type-checks-preprocessor.spec.ts index f5e9e43a6e..1124a5e649 100644 --- a/packages/core/test/unit/sandbox/disable-type-checking-preprocessor.spec.ts +++ b/packages/core/test/unit/sandbox/disable-type-checks-preprocessor.spec.ts @@ -7,17 +7,15 @@ import sinon = require('sinon'); import { expect } from 'chai'; import { coreTokens } from '../../../src/di'; -import { DisableTypeCheckingPreprocessor } from '../../../src/sandbox/disable-type-checking-preprocessor'; +import { DisableTypeChecksPreprocessor } from '../../../src/sandbox/disable-type-checks-preprocessor'; -describe(DisableTypeCheckingPreprocessor.name, () => { - let sut: DisableTypeCheckingPreprocessor; +describe(DisableTypeChecksPreprocessor.name, () => { + let sut: DisableTypeChecksPreprocessor; let disableTypeCheckingStub: sinon.SinonStub; beforeEach(() => { disableTypeCheckingStub = sinon.stub(); - sut = testInjector.injector - .provideValue(coreTokens.disableTypeCheckingHelper, disableTypeCheckingStub) - .injectClass(DisableTypeCheckingPreprocessor); + sut = testInjector.injector.provideValue(coreTokens.disableTypeChecksHelper, disableTypeCheckingStub).injectClass(DisableTypeChecksPreprocessor); }); ['.ts', '.tsx', '.js', '.jsx', '.html', '.vue'].forEach((extension) => { diff --git a/packages/instrumenter/src/disable-type-checking.ts b/packages/instrumenter/src/disable-type-checks.ts similarity index 97% rename from packages/instrumenter/src/disable-type-checking.ts rename to packages/instrumenter/src/disable-type-checks.ts index 3490b0b414..c131c96a1f 100644 --- a/packages/instrumenter/src/disable-type-checking.ts +++ b/packages/instrumenter/src/disable-type-checks.ts @@ -9,7 +9,7 @@ import { AstFormat, HtmlAst, JSAst, TSAst } from './syntax'; const commentDirectiveRegEx = /^(\s*)@(ts-[a-z-]+).*$/; const tsDirectiveLikeRegEx = /@(ts-[a-z-]+)/; -export async function disableTypeChecking(file: File, options: InstrumenterOptions) { +export async function disableTypeChecks(file: File, options: InstrumenterOptions) { if (isJSFileWithoutTSDirectives(file)) { // Performance optimization. Only parse the file when it has a change of containing a `// @ts-` directive return new File(file.name, prefixWithNoCheck(file.textContent)); diff --git a/packages/instrumenter/src/index.ts b/packages/instrumenter/src/index.ts index 6691b99c63..0d4e43ad31 100644 --- a/packages/instrumenter/src/index.ts +++ b/packages/instrumenter/src/index.ts @@ -1,4 +1,4 @@ export * from './instrumenter'; export * from './instrument-result'; export * from './instrumenter-options'; -export * from './disable-type-checking'; +export * from './disable-type-checks'; diff --git a/packages/instrumenter/test/integration/disable-type-checks.it.spec.ts b/packages/instrumenter/test/integration/disable-type-checks.it.spec.ts new file mode 100644 index 0000000000..6ea4077485 --- /dev/null +++ b/packages/instrumenter/test/integration/disable-type-checks.it.spec.ts @@ -0,0 +1,40 @@ +import path from 'path'; +import { promises as fs } from 'fs'; + +import { expect } from 'chai'; +import chaiJestSnapshot from 'chai-jest-snapshot'; + +import { File } from '@stryker-mutator/api/core'; + +import { disableTypeChecks } from '../../src'; +import { createInstrumenterOptions } from '../helpers/factories'; + +const resolveTestResource = path.resolve.bind( + path, + __dirname, + '..' /* integration */, + '..' /* test */, + '..' /* dist */, + 'testResources', + 'disable-type-checks' +); + +describe(`${disableTypeChecks.name} integration`, () => { + it('should be able disable type checks of a type script file', async () => { + await arrangeAndActAssert('app.component.ts'); + }); + it('should be able disable type checks of an html file', async () => { + await arrangeAndActAssert('html-sample.html'); + }); + it('should be able disable type checks of a vue file', async () => { + await arrangeAndActAssert('vue-sample.vue'); + }); + + async function arrangeAndActAssert(fileName: string, options = createInstrumenterOptions()) { + const fullFileName = resolveTestResource(fileName); + const file = new File(fullFileName, await fs.readFile(fullFileName)); + const result = await disableTypeChecks(file, options); + chaiJestSnapshot.setFilename(resolveTestResource(`${fileName}.out.snap`)); + expect(result.textContent).matchSnapshot(); + } +}); diff --git a/packages/instrumenter/test/unit/disable-type-checking.spec.ts b/packages/instrumenter/test/unit/disable-type-checks.spec.ts similarity index 87% rename from packages/instrumenter/test/unit/disable-type-checking.spec.ts rename to packages/instrumenter/test/unit/disable-type-checks.spec.ts index 5dc23d46b9..1c61cd3006 100644 --- a/packages/instrumenter/test/unit/disable-type-checking.spec.ts +++ b/packages/instrumenter/test/unit/disable-type-checks.spec.ts @@ -4,32 +4,32 @@ import sinon from 'sinon'; import { expect } from 'chai'; import * as parsers from '../../src/parsers'; -import { disableTypeChecking } from '../../src'; +import { disableTypeChecks } from '../../src'; -describe(disableTypeChecking.name, () => { +describe(disableTypeChecks.name, () => { describe('with TS or JS AST format', () => { it('should prefix the file with `// @ts-nocheck`', async () => { const inputFile = new File('foo.js', 'foo.bar();'); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.js', '// @ts-nocheck\nfoo.bar();')); }); it('should not even parse the file if "@ts-" can\'t be found anywhere in the file (performance optimization)', async () => { const createParserSpy = sinon.spy(parsers, 'createParser'); const inputFile = new File('foo.js', 'foo.bar();'); - await disableTypeChecking(inputFile, { plugins: null }); + await disableTypeChecks(inputFile, { plugins: null }); expect(createParserSpy).not.called; }); it('should remove @ts directives from a JS file', async () => { const inputFile = new File('foo.js', '// @ts-check\nfoo.bar();'); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.js', '// @ts-nocheck\n// \nfoo.bar();')); }); it('should remove @ts directives from a TS file', async () => { const inputFile = new File('foo.ts', '// @ts-check\nfoo.bar();'); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.ts', '// @ts-nocheck\n// \nfoo.bar();')); }); @@ -89,7 +89,7 @@ describe(disableTypeChecking.name, () => { async function arrangeActAssert(input: string, expectedOutput = input) { const inputFile = new File('foo.tsx', input); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.tsx', `// @ts-nocheck\n${expectedOutput}`)); } }); @@ -97,13 +97,13 @@ describe(disableTypeChecking.name, () => { describe('with HTML ast format', () => { it('should prefix the script tags with `// @ts-nocheck`', async () => { const inputFile = new File('foo.vue', ''); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.vue', '')); }); it('should remove `// @ts` directives from script tags', async () => { const inputFile = new File('foo.html', ''); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual( actual, new File('foo.html', '') @@ -112,7 +112,7 @@ describe(disableTypeChecking.name, () => { it('should not remove `// @ts` from the html itself', async () => { const inputFile = new File('foo.vue', ''); - const actual = await disableTypeChecking(inputFile, { plugins: null }); + const actual = await disableTypeChecks(inputFile, { plugins: null }); assertions.expectTextFileEqual(actual, new File('foo.vue', '')); }); }); From 22c5874472557812e9c45cd5af3de70260429436 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Sep 2020 21:21:09 +0200 Subject: [PATCH 13/13] Add disable-type-checks integration tests --- .../disable-type-checks/app.component.ts | 28 ++++ .../app.component.ts.out.snap | 67 +++++++++ .../disable-type-checks/html-sample.html | 20 +++ .../html-sample.html.out.snap | 53 +++++++ .../disable-type-checks/vue-sample.vue | 118 ++++++++++++++++ .../vue-sample.vue.out.snap | 129 ++++++++++++++++++ 6 files changed, 415 insertions(+) create mode 100644 packages/instrumenter/testResources/disable-type-checks/app.component.ts create mode 100644 packages/instrumenter/testResources/disable-type-checks/app.component.ts.out.snap create mode 100644 packages/instrumenter/testResources/disable-type-checks/html-sample.html create mode 100644 packages/instrumenter/testResources/disable-type-checks/html-sample.html.out.snap create mode 100644 packages/instrumenter/testResources/disable-type-checks/vue-sample.vue create mode 100644 packages/instrumenter/testResources/disable-type-checks/vue-sample.vue.out.snap diff --git a/packages/instrumenter/testResources/disable-type-checks/app.component.ts b/packages/instrumenter/testResources/disable-type-checks/app.component.ts new file mode 100644 index 0000000000..6ddba23c4b --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/app.component.ts @@ -0,0 +1,28 @@ +import {Component, HostListener, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + +@Component({ + selector: 'ksw-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'Kantishop'; + + constructor(@Inject(DOCUMENT) document) { + } + + @HostListener('window:scroll', ['$event']) + onWindowScroll(e) { + // @ts-expect-error + if (window.pageYOffset > document.getElementById('banner').offsetHeight) { + const element = document.getElementById('kanti-menu'); + element.classList.add('kanti-sticky'); + document.getElementsByTagName('main').item(0).setAttribute('style', 'margin-top: 50px'); + } else { + const element = document.getElementById('kanti-menu'); + element.classList.remove('kanti-sticky'); + document.getElementsByTagName('main').item(0).removeAttribute('style'); + } + } +} diff --git a/packages/instrumenter/testResources/disable-type-checks/app.component.ts.out.snap b/packages/instrumenter/testResources/disable-type-checks/app.component.ts.out.snap new file mode 100644 index 0000000000..0891124a73 --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/app.component.ts.out.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disableTypeChecks integration should be able disable type checking of a type script file 1`] = ` +"// @ts-nocheck +import {Component, HostListener, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + +@Component({ + selector: 'ksw-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'Kantishop'; + + constructor(@Inject(DOCUMENT) document) { + } + + @HostListener('window:scroll', ['$event']) + onWindowScroll(e) { + // + if (window.pageYOffset > document.getElementById('banner').offsetHeight) { + const element = document.getElementById('kanti-menu'); + element.classList.add('kanti-sticky'); + document.getElementsByTagName('main').item(0).setAttribute('style', 'margin-top: 50px'); + } else { + const element = document.getElementById('kanti-menu'); + element.classList.remove('kanti-sticky'); + document.getElementsByTagName('main').item(0).removeAttribute('style'); + } + } +} +" +`; + +exports[`disableTypeChecks integration should be able disable type checks of a type script file 1`] = ` +"// @ts-nocheck +import {Component, HostListener, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + +@Component({ + selector: 'ksw-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'Kantishop'; + + constructor(@Inject(DOCUMENT) document) { + } + + @HostListener('window:scroll', ['$event']) + onWindowScroll(e) { + // + if (window.pageYOffset > document.getElementById('banner').offsetHeight) { + const element = document.getElementById('kanti-menu'); + element.classList.add('kanti-sticky'); + document.getElementsByTagName('main').item(0).setAttribute('style', 'margin-top: 50px'); + } else { + const element = document.getElementById('kanti-menu'); + element.classList.remove('kanti-sticky'); + document.getElementsByTagName('main').item(0).removeAttribute('style'); + } + } +} +" +`; diff --git a/packages/instrumenter/testResources/disable-type-checks/html-sample.html b/packages/instrumenter/testResources/disable-type-checks/html-sample.html new file mode 100644 index 0000000000..dd95a9ab66 --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/html-sample.html @@ -0,0 +1,20 @@ + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/packages/instrumenter/testResources/disable-type-checks/html-sample.html.out.snap b/packages/instrumenter/testResources/disable-type-checks/html-sample.html.out.snap new file mode 100644 index 0000000000..f4eb2c49a9 --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/html-sample.html.out.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disableTypeChecks integration should be able disable type checking of an html file 1`] = ` +" + + + + + + Document + + + + + + +" +`; + +exports[`disableTypeChecks integration should be able disable type checks of an html file 1`] = ` +" + + + + + + Document + + + + + + +" +`; diff --git a/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue b/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue new file mode 100644 index 0000000000..04293cd27a --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue @@ -0,0 +1,118 @@ + + + + + + + diff --git a/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue.out.snap b/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue.out.snap new file mode 100644 index 0000000000..96213eaf66 --- /dev/null +++ b/packages/instrumenter/testResources/disable-type-checks/vue-sample.vue.out.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disableTypeChecks integration should be able disable type checks of a vue file 1`] = ` +" + + + + + + +" +`;