From 2bd6128ad6c88a3a4977db4d02b385a454ffeaac Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Tue, 4 Sep 2018 22:10:21 +0200 Subject: [PATCH] Support project references (#392) --- .../references/default/e/index.ts.lint | 4 + .../project/references/filtered/a/a.ts.lint | 4 + .../references/filtered/a/ambient.d.ts.lint | 2 + .../references/filtered/c/index.ts.lint | 8 + .../references/filtered/e/index.ts.lint | 4 + .../references/missing/missing/y.ts.fix | 4 + .../references/missing/missing/y.ts.lint | 7 + .../references/outfile/outfile/b.ts.lint | 4 + .../project/references/references/a/a.ts.fix | 1 + .../project/references/references/a/a.ts.lint | 4 + .../references/references/a/ambient.d.ts.lint | 2 + .../references/references/a/index.ts.lint | 2 + .../references/references/b/index.tsx.lint | 4 + .../references/references/c/index.ts.lint | 8 + .../references/references/d/index.ts.lint | 4 + .../references/references/e/index.ts.lint | 4 + .../references/references/outfile/a.ts.lint | 4 + .../references/references/outfile/b.ts.lint | 4 + packages/wotan/README.md | 6 +- packages/wotan/src/argparse.ts | 14 + packages/wotan/src/commands/save.ts | 5 +- packages/wotan/src/commands/test.ts | 1 + packages/wotan/src/project-host.ts | 48 +++- packages/wotan/src/runner.ts | 261 +++++++++++++----- packages/wotan/src/utils.ts | 17 +- packages/wotan/test/argparse.spec.ts | 67 ++++- packages/wotan/test/commands.spec.ts | 9 + .../test/project/references/.wotanrc.yaml | 5 + packages/wotan/test/project/references/a/a.ts | 3 + .../test/project/references/a/ambient.d.ts | 1 + .../wotan/test/project/references/a/index.ts | 1 + .../test/project/references/a/tsconfig.json | 8 + .../wotan/test/project/references/b/index.tsx | 3 + .../test/project/references/b/tsconfig.json | 10 + .../wotan/test/project/references/c/index.ts | 6 + .../test/project/references/c/tsconfig.json | 10 + .../wotan/test/project/references/d/index.ts | 3 + .../test/project/references/d/tsconfig.json | 9 + .../test/project/references/default.test.json | 5 + .../wotan/test/project/references/e/index.ts | 3 + .../test/project/references/e/tsconfig.json | 9 + .../project/references/filtered.test.json | 7 + .../test/project/references/missing.test.json | 4 + .../project/references/missing/.wotanrc.yaml | 3 + .../references/missing/tsconfig.x.json | 11 + .../references/missing/tsconfig.y.json | 9 + .../test/project/references/missing/x.d.ts | 4 + .../test/project/references/missing/x.js | 9 + .../test/project/references/missing/x.ts | 3 + .../test/project/references/missing/y.ts | 4 + .../wotan/test/project/references/out-a/a.js | 10 + .../test/project/references/out-a/index.js | 6 + .../test/project/references/outdir/a/a.d.ts | 2 + .../project/references/outdir/a/index.d.ts | 1 + .../project/references/outdir/b/index.d.ts | 3 + .../test/project/references/outdir/b/index.js | 24 ++ .../project/references/outdir/c/index.d.ts | 5 + .../test/project/references/outdir/c/index.js | 27 ++ .../project/references/outdir/d/index.d.ts | 3 + .../test/project/references/outdir/d/index.js | 24 ++ .../test/project/references/outfile.test.json | 3 + .../test/project/references/outfile/a.ts | 3 + .../test/project/references/outfile/b.ts | 3 + .../project/references/outfile/out-a.d.ts | 3 + .../test/project/references/outfile/out-a.js | 4 + .../references/outfile/tsconfig.a.json | 9 + .../references/outfile/tsconfig.b.json | 11 + .../test/project/references/quotemark.js | 11 + .../project/references/references.test.json | 4 + .../test/project/references/tsconfig.json | 12 + packages/wotan/test/runner.spec.ts | 13 +- 71 files changed, 724 insertions(+), 89 deletions(-) create mode 100644 baselines/packages/wotan/test/project/references/default/e/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/filtered/a/a.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/filtered/a/ambient.d.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/filtered/c/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/filtered/e/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/missing/missing/y.ts.fix create mode 100644 baselines/packages/wotan/test/project/references/missing/missing/y.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/outfile/outfile/b.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/a/a.ts.fix create mode 100644 baselines/packages/wotan/test/project/references/references/a/a.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/a/ambient.d.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/a/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/b/index.tsx.lint create mode 100644 baselines/packages/wotan/test/project/references/references/c/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/d/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/e/index.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/outfile/a.ts.lint create mode 100644 baselines/packages/wotan/test/project/references/references/outfile/b.ts.lint create mode 100644 packages/wotan/test/project/references/.wotanrc.yaml create mode 100644 packages/wotan/test/project/references/a/a.ts create mode 100644 packages/wotan/test/project/references/a/ambient.d.ts create mode 100644 packages/wotan/test/project/references/a/index.ts create mode 100644 packages/wotan/test/project/references/a/tsconfig.json create mode 100644 packages/wotan/test/project/references/b/index.tsx create mode 100644 packages/wotan/test/project/references/b/tsconfig.json create mode 100644 packages/wotan/test/project/references/c/index.ts create mode 100644 packages/wotan/test/project/references/c/tsconfig.json create mode 100644 packages/wotan/test/project/references/d/index.ts create mode 100644 packages/wotan/test/project/references/d/tsconfig.json create mode 100644 packages/wotan/test/project/references/default.test.json create mode 100644 packages/wotan/test/project/references/e/index.ts create mode 100644 packages/wotan/test/project/references/e/tsconfig.json create mode 100644 packages/wotan/test/project/references/filtered.test.json create mode 100644 packages/wotan/test/project/references/missing.test.json create mode 100644 packages/wotan/test/project/references/missing/.wotanrc.yaml create mode 100644 packages/wotan/test/project/references/missing/tsconfig.x.json create mode 100644 packages/wotan/test/project/references/missing/tsconfig.y.json create mode 100644 packages/wotan/test/project/references/missing/x.d.ts create mode 100644 packages/wotan/test/project/references/missing/x.js create mode 100644 packages/wotan/test/project/references/missing/x.ts create mode 100644 packages/wotan/test/project/references/missing/y.ts create mode 100644 packages/wotan/test/project/references/out-a/a.js create mode 100644 packages/wotan/test/project/references/out-a/index.js create mode 100644 packages/wotan/test/project/references/outdir/a/a.d.ts create mode 100644 packages/wotan/test/project/references/outdir/a/index.d.ts create mode 100644 packages/wotan/test/project/references/outdir/b/index.d.ts create mode 100644 packages/wotan/test/project/references/outdir/b/index.js create mode 100644 packages/wotan/test/project/references/outdir/c/index.d.ts create mode 100644 packages/wotan/test/project/references/outdir/c/index.js create mode 100644 packages/wotan/test/project/references/outdir/d/index.d.ts create mode 100644 packages/wotan/test/project/references/outdir/d/index.js create mode 100644 packages/wotan/test/project/references/outfile.test.json create mode 100644 packages/wotan/test/project/references/outfile/a.ts create mode 100644 packages/wotan/test/project/references/outfile/b.ts create mode 100644 packages/wotan/test/project/references/outfile/out-a.d.ts create mode 100644 packages/wotan/test/project/references/outfile/out-a.js create mode 100644 packages/wotan/test/project/references/outfile/tsconfig.a.json create mode 100644 packages/wotan/test/project/references/outfile/tsconfig.b.json create mode 100644 packages/wotan/test/project/references/quotemark.js create mode 100644 packages/wotan/test/project/references/references.test.json create mode 100644 packages/wotan/test/project/references/tsconfig.json diff --git a/baselines/packages/wotan/test/project/references/default/e/index.ts.lint b/baselines/packages/wotan/test/project/references/default/e/index.ts.lint new file mode 100644 index 000000000..f6e182b58 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/default/e/index.ts.lint @@ -0,0 +1,4 @@ +import { D } from "../d"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class E extends D {} diff --git a/baselines/packages/wotan/test/project/references/filtered/a/a.ts.lint b/baselines/packages/wotan/test/project/references/filtered/a/a.ts.lint new file mode 100644 index 000000000..1712be4af --- /dev/null +++ b/baselines/packages/wotan/test/project/references/filtered/a/a.ts.lint @@ -0,0 +1,4 @@ +export class A {} +// this fixable failure ensures we only lint this project / file once +debugger; +~~~~~~~~~ [error no-debugger: 'debugger' statements are forbidden.] diff --git a/baselines/packages/wotan/test/project/references/filtered/a/ambient.d.ts.lint b/baselines/packages/wotan/test/project/references/filtered/a/ambient.d.ts.lint new file mode 100644 index 000000000..7152d6838 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/filtered/a/ambient.d.ts.lint @@ -0,0 +1,2 @@ +declare var someGlobal: "a"; + ~~~ [error local/quotemark: Prefer single quotes] diff --git a/baselines/packages/wotan/test/project/references/filtered/c/index.ts.lint b/baselines/packages/wotan/test/project/references/filtered/c/index.ts.lint new file mode 100644 index 000000000..0b8e61c2d --- /dev/null +++ b/baselines/packages/wotan/test/project/references/filtered/c/index.ts.lint @@ -0,0 +1,8 @@ +import { B } from "../b"; + ~~~~~~ [error local/quotemark: Prefer single quotes] +import { A } from "../a"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class C extends B { + prop = new A(); +} diff --git a/baselines/packages/wotan/test/project/references/filtered/e/index.ts.lint b/baselines/packages/wotan/test/project/references/filtered/e/index.ts.lint new file mode 100644 index 000000000..f6e182b58 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/filtered/e/index.ts.lint @@ -0,0 +1,4 @@ +import { D } from "../d"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class E extends D {} diff --git a/baselines/packages/wotan/test/project/references/missing/missing/y.ts.fix b/baselines/packages/wotan/test/project/references/missing/missing/y.ts.fix new file mode 100644 index 000000000..35564bd44 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/missing/missing/y.ts.fix @@ -0,0 +1,4 @@ +import { X } from "./x"; + +// this needs two fixer runs and ensures it still resolves to 'x.d.ts' instead of 'x.ts' +new X().prop; diff --git a/baselines/packages/wotan/test/project/references/missing/missing/y.ts.lint b/baselines/packages/wotan/test/project/references/missing/missing/y.ts.lint new file mode 100644 index 000000000..d09567eca --- /dev/null +++ b/baselines/packages/wotan/test/project/references/missing/missing/y.ts.lint @@ -0,0 +1,7 @@ +import { X } from "./x"; + ~~~~~ [error local/quotemark: Prefer single quotes] + +// this needs two fixer runs and ensures it still resolves to 'x.d.ts' instead of 'x.ts' +new X().prop; +~~~~~~~~ [error no-useless-assertion: This assertion is unnecesary as it doesn't change the type of the expression.] + ~~~~~~~~ [error no-useless-assertion: This assertion is unnecesary as it doesn't change the type of the expression.] diff --git a/baselines/packages/wotan/test/project/references/outfile/outfile/b.ts.lint b/baselines/packages/wotan/test/project/references/outfile/outfile/b.ts.lint new file mode 100644 index 000000000..bb85752e7 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/outfile/outfile/b.ts.lint @@ -0,0 +1,4 @@ +namespace foo { + console.log("a:", a); + ~~~~ [error local/quotemark: Prefer single quotes] +} diff --git a/baselines/packages/wotan/test/project/references/references/a/a.ts.fix b/baselines/packages/wotan/test/project/references/references/a/a.ts.fix new file mode 100644 index 000000000..1e14df544 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/a/a.ts.fix @@ -0,0 +1 @@ +export class A {} diff --git a/baselines/packages/wotan/test/project/references/references/a/a.ts.lint b/baselines/packages/wotan/test/project/references/references/a/a.ts.lint new file mode 100644 index 000000000..1712be4af --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/a/a.ts.lint @@ -0,0 +1,4 @@ +export class A {} +// this fixable failure ensures we only lint this project / file once +debugger; +~~~~~~~~~ [error no-debugger: 'debugger' statements are forbidden.] diff --git a/baselines/packages/wotan/test/project/references/references/a/ambient.d.ts.lint b/baselines/packages/wotan/test/project/references/references/a/ambient.d.ts.lint new file mode 100644 index 000000000..7152d6838 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/a/ambient.d.ts.lint @@ -0,0 +1,2 @@ +declare var someGlobal: "a"; + ~~~ [error local/quotemark: Prefer single quotes] diff --git a/baselines/packages/wotan/test/project/references/references/a/index.ts.lint b/baselines/packages/wotan/test/project/references/references/a/index.ts.lint new file mode 100644 index 000000000..cf5ee50d1 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/a/index.ts.lint @@ -0,0 +1,2 @@ +export * from "./a"; + ~~~~~ [error local/quotemark: Prefer single quotes] diff --git a/baselines/packages/wotan/test/project/references/references/b/index.tsx.lint b/baselines/packages/wotan/test/project/references/references/b/index.tsx.lint new file mode 100644 index 000000000..8e15703af --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/b/index.tsx.lint @@ -0,0 +1,4 @@ +import { A } from "../a"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class B extends A {} diff --git a/baselines/packages/wotan/test/project/references/references/c/index.ts.lint b/baselines/packages/wotan/test/project/references/references/c/index.ts.lint new file mode 100644 index 000000000..0b8e61c2d --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/c/index.ts.lint @@ -0,0 +1,8 @@ +import { B } from "../b"; + ~~~~~~ [error local/quotemark: Prefer single quotes] +import { A } from "../a"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class C extends B { + prop = new A(); +} diff --git a/baselines/packages/wotan/test/project/references/references/d/index.ts.lint b/baselines/packages/wotan/test/project/references/references/d/index.ts.lint new file mode 100644 index 000000000..2acca85bb --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/d/index.ts.lint @@ -0,0 +1,4 @@ +import { C } from "../c"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class D extends C {} diff --git a/baselines/packages/wotan/test/project/references/references/e/index.ts.lint b/baselines/packages/wotan/test/project/references/references/e/index.ts.lint new file mode 100644 index 000000000..f6e182b58 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/e/index.ts.lint @@ -0,0 +1,4 @@ +import { D } from "../d"; + ~~~~~~ [error local/quotemark: Prefer single quotes] + +export class E extends D {} diff --git a/baselines/packages/wotan/test/project/references/references/outfile/a.ts.lint b/baselines/packages/wotan/test/project/references/references/outfile/a.ts.lint new file mode 100644 index 000000000..a3b5d87a8 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/outfile/a.ts.lint @@ -0,0 +1,4 @@ +namespace foo { + export const a = "a"; + ~~~ [error local/quotemark: Prefer single quotes] +} diff --git a/baselines/packages/wotan/test/project/references/references/outfile/b.ts.lint b/baselines/packages/wotan/test/project/references/references/outfile/b.ts.lint new file mode 100644 index 000000000..bb85752e7 --- /dev/null +++ b/baselines/packages/wotan/test/project/references/references/outfile/b.ts.lint @@ -0,0 +1,4 @@ +namespace foo { + console.log("a:", a); + ~~~~ [error local/quotemark: Prefer single quotes] +} diff --git a/packages/wotan/README.md b/packages/wotan/README.md index 45d0336a2..d28e5f301 100644 --- a/packages/wotan/README.md +++ b/packages/wotan/README.md @@ -36,6 +36,7 @@ Now you can run the linter with one of the following commands depending on your wotan -p # lint the whole project wotan 'src/**/*.ts' -e '**/*.d.ts' # lint all typescript files excluding declaration files wotan --fix # lint the whole project and fix all fixable errors +wotan -p tsconfig.json -r # lint the specified project and all projects in its 'references' ``` ## Available Rules @@ -134,12 +135,13 @@ Sometimes you need to enable or disable a specific rule or all rules for a secti ## CLI Options -* `-m --module ` specifies one or more packages with DI modules to load before starting the actual linter. These modules can be used to override the default behavior. * `-c --config ` specifies the configuration to use for all files instead of looking for configuration files in parent directories. This can either be a file name, the name of a node module containing a shareable config, or the name of a builtin config like `wotan:recommended` * `-e --exclude ` excludes all files that match the given glob pattern from linting. This option can be used multiple times to specify multiple patterns. For example `-e '**/*.js' -e '**/*.d.ts'`. It is recommended to wrap the glob patterns in single quotes to prevent the shell from expanding them. +* `--fix [true|false|number]` automatically fixes all fixable failures in your code and writes the result back to disk. There are some precautions to prevent overlapping fixes from destroying you code. You should however commit your changes before using this feature. Given a number it will at most use the specified number of iterations for fixing before returning the result. * `-f --formatter ` the name or path of a formatter. This can either be a file name, the name of a node module contianing a formatter, or the name of a builtin formatter. Currently available builtin formatters are `json` and `stylish` (default). -* `--fix [true|false]` automatically fixes all fixable failures in your code and writes the result back to disk. There are some precautions to prevent overlapping fixes from destroying you code. You should however commit your changes before using this feature. +* `-m --module ` specifies one or more packages with DI modules to load before starting the actual linter. These modules can be used to override the default behavior. * `-p --project ` specifies the path to the `tsconfig.json` file to use. This option is used to find all files contained in your project. It also enables rules that require type information. +* `-r --references [true|false]` enables project references. Starting from the project specified with `-p --project` or the `tsconfig.json` in the current directory it will recursively follow all `"references"` and lint those projects. * `[...FILES]` specifies the files to lint. You can specify paths and glob patterns here. Note that all file paths are relative to the current working directory. Therefore `**/*.ts` doesn't match `../foo.ts`. diff --git a/packages/wotan/src/argparse.ts b/packages/wotan/src/argparse.ts index b3a32cba1..84c6dafbc 100644 --- a/packages/wotan/src/argparse.ts +++ b/packages/wotan/src/argparse.ts @@ -50,6 +50,7 @@ export function parseGlobalOptions(options: GlobalOptions | undefined): ParsedGl files: [], exclude: [], project: undefined, + references: false, formatter: undefined, fix: false, extensions: undefined, @@ -60,6 +61,7 @@ export function parseGlobalOptions(options: GlobalOptions | undefined): ParsedGl files: expectStringOrStringArray(options, 'files') || [], exclude: expectStringOrStringArray(options, 'exclude') || [], project: expectStringOption(options, 'project'), + references: expectBooleanOption(options, 'references'), formatter: expectStringOption(options, 'formatter'), fix: expectBooleanOrNumberOption(options, 'fix'), extensions: (expectStringOrStringArray(options, 'extensions') || []).map(sanitizeExtensionArgument), @@ -84,6 +86,14 @@ function expectStringOption(options: GlobalOptions, option: string): string | un log("Expected a value of type 'string' for option '%s'.", option); return; } +function expectBooleanOption(options: GlobalOptions, option: string): boolean { + const value = options[option]; + if (typeof value === 'boolean') + return value; + if (value !== undefined) + log("Expected a value of type 'boolean' for option '%s'.", option); + return false; +} function expectBooleanOrNumberOption(options: GlobalOptions, option: string): boolean | number { const value = options[option]; if (typeof value === 'boolean' || typeof value === 'number') @@ -117,6 +127,10 @@ function parseLintCommand( case '--project': result.project = expectStringArgument(args, ++i, arg) || undefined; break; + case '-r': + case '--references': + ({index: i, argument: result.references} = parseOptionalBoolean(args, i)); + break; case '-e': case '--exclude': result.exclude = exclude; diff --git a/packages/wotan/src/commands/save.ts b/packages/wotan/src/commands/save.ts index cd7ab0550..329b4ba7f 100644 --- a/packages/wotan/src/commands/save.ts +++ b/packages/wotan/src/commands/save.ts @@ -17,7 +17,10 @@ class SaveCommandRunner extends AbstractCommandRunner { } public run({command: _command, ...config}: LintCommand) { - const newContent = format({...this.options, ...config, fix: config.fix || undefined}, Format.Yaml); + const newContent = format( + {...this.options, ...config, fix: config.fix || undefined, references: config.references || undefined}, + Format.Yaml, + ); const filePath = path.join(this.directories.getCurrentDirectory(), '.fimbullinter.yaml'); if (newContent.trim() === '{}') { try { diff --git a/packages/wotan/src/commands/test.ts b/packages/wotan/src/commands/test.ts index c64af2c4f..0cea2b00b 100644 --- a/packages/wotan/src/commands/test.ts +++ b/packages/wotan/src/commands/test.ts @@ -153,6 +153,7 @@ class TestCommandRunner extends AbstractCommandRunner { exclude: [], files: [], project: undefined, + references: false, extensions: undefined, ...config, fix: false, diff --git a/packages/wotan/src/project-host.ts b/packages/wotan/src/project-host.ts index 0566d0870..3294bb08a 100644 --- a/packages/wotan/src/project-host.ts +++ b/packages/wotan/src/project-host.ts @@ -1,5 +1,5 @@ import * as ts from 'typescript'; -import { resolveCachedResult, hasSupportedExtension } from './utils'; +import { resolveCachedResult, hasSupportedExtension, mapDefined } from './utils'; import * as path from 'path'; import { ProcessorLoader } from './services/processor-loader'; import { FileKind, CachedFileSystem } from './services/cached-file-system'; @@ -39,8 +39,23 @@ export class ProjectHost implements ts.CompilerHost { public getProcessedFileInfo(fileName: string) { return this.processedFiles.get(fileName); } - public getDirectoryEntries(dir: string): ts.FileSystemEntries { - return resolveCachedResult(this.directoryEntries, dir, this.processDirectory); + public readDirectory( + rootDir: string, + extensions: ReadonlyArray, + excludes: ReadonlyArray | undefined, + includes: ReadonlyArray, + depth?: number, + ) { + return ts.matchFiles( + rootDir, + extensions, + excludes, + includes, + this.useCaseSensitiveFileNames(), + this.cwd, + depth, + (dir) => resolveCachedResult(this.directoryEntries, dir, this.processDirectory), + ); } /** * Try to find and load the configuration for a file. @@ -191,6 +206,23 @@ export class ProjectHost implements ts.CompilerHost { ); } + public createProgram( + rootNames: ReadonlyArray, + options: ts.CompilerOptions, + oldProgram: ts.Program | undefined, + projectReferences: ReadonlyArray | undefined, + ) { + return projectReferences === undefined + ? ts.createProgram(rootNames, options, this, oldProgram) // for compatibility with TypeScript@<3.0.0 + : ts.createProgram({ + rootNames, + options, + oldProgram, + projectReferences, + host: this, + }); + } + public updateSourceFile( sourceFile: ts.SourceFile, program: ts.Program, @@ -200,7 +232,15 @@ export class ProjectHost implements ts.CompilerHost { // TODO use updateSourceFile once https://github.com/Microsoft/TypeScript/issues/26166 is resolved sourceFile = ts.createSourceFile(sourceFile.fileName, newContent, sourceFile.languageVersion, true); this.sourceFileCache.set(sourceFile.fileName, sourceFile); - program = ts.createProgram(program.getRootFileNames(), program.getCompilerOptions(), this, program); + const references = program.getProjectReferences && // for compatibility with TypeScript@<3.0.0 + program.getProjectReferences(); + + program = this.createProgram( + program.getRootFileNames(), + program.getCompilerOptions(), + program, + references && mapDefined(references, (ref) => ref && {path: ref.sourceFile.fileName}), + ); return {sourceFile, program}; } diff --git a/packages/wotan/src/runner.ts b/packages/wotan/src/runner.ts index 4acf514bc..f3d30e47c 100644 --- a/packages/wotan/src/runner.ts +++ b/packages/wotan/src/runner.ts @@ -11,7 +11,7 @@ import { import * as path from 'path'; import * as ts from 'typescript'; import * as glob from 'glob'; -import { unixifyPath, hasSupportedExtension } from './utils'; +import { unixifyPath, hasSupportedExtension, mapDefined } from './utils'; import { Minimatch, IMinimatch } from 'minimatch'; import { ProcessorLoader } from './services/processor-loader'; import { injectable } from 'inversify'; @@ -28,6 +28,7 @@ export interface LintOptions { files: string[]; exclude: string[]; project: string | undefined; + references: boolean; fix: boolean | number; extensions: string[] | undefined; } @@ -66,48 +67,50 @@ export class Runner { this.configManager, this.processorLoader, ); - let {files, program} = this.getFilesAndProgram(options.project, options.files, options.exclude, processorHost); - - for (const file of files) { - if (!hasSupportedExtension(file)) - continue; - if (options.config === undefined) - config = this.configManager.find(file); - const mapped = processorHost.getProcessedFileInfo(file); - const originalName = mapped === undefined ? file : mapped.originalName; - const effectiveConfig = config && this.configManager.reduce(config, originalName); - if (effectiveConfig === undefined) - continue; - let sourceFile = program.getSourceFile(file)!; - const originalContent = mapped === undefined ? sourceFile.text : mapped.originalContent; - let summary: FileSummary; - const fix = shouldFix(sourceFile, options, originalName); - if (fix) { - summary = this.linter.lintAndFix( - sourceFile, - originalContent, - effectiveConfig, - (content, range) => { - ({sourceFile, program} = processorHost.updateSourceFile(sourceFile, program, content, range)); - return {program, file: sourceFile}; - }, - fix === true ? undefined : fix, - program, - mapped === undefined ? undefined : mapped.processor, - ); - } else { - summary = { - failures: this.linter.getFailures( + for (let {files, program} of + this.getFilesAndProgram(options.project, options.files, options.exclude, processorHost, options.references) + ) { + for (const file of files) { + if (!hasSupportedExtension(file)) + continue; + if (options.config === undefined) + config = this.configManager.find(file); + const mapped = processorHost.getProcessedFileInfo(file); + const originalName = mapped === undefined ? file : mapped.originalName; + const effectiveConfig = config && this.configManager.reduce(config, originalName); + if (effectiveConfig === undefined) + continue; + let sourceFile = program.getSourceFile(file)!; + const originalContent = mapped === undefined ? sourceFile.text : mapped.originalContent; + let summary: FileSummary; + const fix = shouldFix(sourceFile, options, originalName); + if (fix) { + summary = this.linter.lintAndFix( sourceFile, + originalContent, effectiveConfig, + (content, range) => { + ({sourceFile, program} = processorHost.updateSourceFile(sourceFile, program, content, range)); + return {program, file: sourceFile}; + }, + fix === true ? undefined : fix, program, mapped === undefined ? undefined : mapped.processor, - ), - fixes: 0, - content: originalContent, - }; + ); + } else { + summary = { + failures: this.linter.getFailures( + sourceFile, + effectiveConfig, + program, + mapped === undefined ? undefined : mapped.processor, + ), + fixes: 0, + content: originalContent, + }; + } + yield [originalName, summary]; } - yield [originalName, summary]; } } @@ -184,46 +187,63 @@ export class Runner { } } - private getFilesAndProgram( + private* getFilesAndProgram( project: string | undefined, patterns: string[], exclude: string[], host: ProjectHost, - ): {files: Iterable, program: ts.Program} { + references: boolean, + ): Iterable<{files: Iterable, program: ts.Program}> { const cwd = this.directories.getCurrentDirectory(); if (project !== undefined) { project = this.checkConfigDirectory(path.resolve(cwd, project)); + } else if (references) { + project = this.checkConfigDirectory(cwd); } else { project = ts.findConfigFile(cwd, (f) => this.fs.isFile(f)); if (project === undefined) throw new ConfigurationError(`Cannot find tsconfig.json for directory '${cwd}'.`); } - const program = this.createProgram(project, host); - const files: string[] = []; + const originalNames: string [] = []; - const libDirectory = unixifyPath(path.dirname(ts.getDefaultLibFilePath(program.getCompilerOptions()))) + '/'; const include = patterns.map((p) => new Minimatch(p)); const ex = exclude.map((p) => new Minimatch(p, {dot: true})); - const typeRoots = ts.getEffectiveTypeRoots(program.getCompilerOptions(), host) || []; + // TODO maybe use a different host for each Program or purge all non-declaration files? + for (const program of this.createPrograms(project, host, new Set(), references, isFileIncluded)) { + const options = program.getCompilerOptions(); + const files: string[] = []; + const libDirectory = unixifyPath(path.dirname(ts.getDefaultLibFilePath(options))) + '/'; + const typeRoots = ts.getEffectiveTypeRoots(options, host) || []; + const rootFileNames = program.getRootFileNames(); + const outputsOfReferencedProjects = getOutputsOfProjectReferences(program, host); - for (const sourceFile of program.getSourceFiles()) { - const {fileName} = sourceFile; - if ( - fileName.startsWith(libDirectory) || // lib.xxx.d.ts - // tslib implicitly gets added while linting a project where a dependency in node_modules contains typescript files - fileName.endsWith('/node_modules/tslib/tslib.d.ts') || - program.isSourceFileFromExternalLibrary(sourceFile) || - !typeRoots.every((typeRoot) => path.relative(typeRoot, fileName).startsWith('..' + path.sep)) - ) - continue; - const originalName = host.getFileSystemFile(fileName)!; - if (include.length !== 0 && !include.some((e) => e.match(originalName)) || ex.some((e) => e.match(originalName))) - continue; - files.push(fileName); - originalNames.push(originalName); + for (const sourceFile of program.getSourceFiles()) { + const {fileName} = sourceFile; + if ( + options.composite && !rootFileNames.includes(fileName) || // composite projects need to specify all files as rootFiles + program.isSourceFileFromExternalLibrary(sourceFile) || + fileName.endsWith('.d.ts') && ( + fileName.startsWith(libDirectory) || // lib.xxx.d.ts + // tslib implicitly gets added while linting a project where a dependency in node_modules contains typescript files + fileName.endsWith('/node_modules/tslib/tslib.d.ts') || + outputsOfReferencedProjects.includes(fileName) || + typeRoots.some((typeRoot) => !path.relative(typeRoot, fileName).startsWith('..' + path.sep)) + ) + ) + continue; + const originalName = host.getFileSystemFile(fileName)!; + if (!isFileIncluded(originalName)) + continue; + files.push(fileName); + originalNames.push(originalName); + } + yield {files, program}; } ensurePatternsMatch(include, ex, originalNames); - return {files, program}; + + function isFileIncluded(fileName: string) { + return (include.length === 0 || include.some((p) => p.match(fileName))) && !ex.some((p) => p.match(fileName)); + } } private checkConfigDirectory(fileOrDirName: string): string { @@ -231,7 +251,7 @@ export class Runner { case FileKind.NonExistent: throw new ConfigurationError(`The specified path does not exist: '${fileOrDirName}'`); case FileKind.Directory: { - const file = path.join(fileOrDirName, 'tsconfig.json'); + const file = unixifyPath(path.join(fileOrDirName, 'tsconfig.json')); if (!this.fs.isFile(file)) throw new ConfigurationError(`Cannot find a tsconfig.json file at the specified directory: '${fileOrDirName}'`); return file; @@ -241,7 +261,17 @@ export class Runner { } } - private createProgram(configFile: string, host: ProjectHost): ts.Program { + private* createPrograms( + configFile: string, + host: ProjectHost, + seen: Set, + references: boolean, + isFileIncluded: (fileName: string) => boolean, + ): Iterable { + if (seen.has(configFile)) + return; + seen.add(configFile); + const config = ts.readConfigFile(configFile, (file) => host.readFile(file)); if (config.error !== undefined) { this.logger.warn(ts.formatDiagnostics([config.error], host)); @@ -254,12 +284,103 @@ export class Runner { {noEmit: true}, configFile, ); - if (parsed.errors.length !== 0) - this.logger.warn(ts.formatDiagnostics(parsed.errors, host)); - return ts.createProgram(parsed.fileNames, parsed.options, host); + if (parsed.errors.length !== 0) { + let {errors} = parsed; + if (references && parsed.projectReferences !== undefined && parsed.projectReferences.length !== 0) + errors = errors.filter((e) => e.code !== 18002); // 'files' is allowed to be empty if there are project references + if (errors.length !== 0) + this.logger.warn(ts.formatDiagnostics(parsed.errors, host)); + } + if (parsed.fileNames.length !== 0) { + if (parsed.options.composite && !parsed.fileNames.some((file) => isFileIncluded(host.getFileSystemFile(file)!))) { + log("Project '%s' contains no file to lint", configFile); + } else { + log("Using project '%s'", configFile); + yield host.createProgram(parsed.fileNames, parsed.options, undefined, parsed.projectReferences); + } + } + if (references && parsed.projectReferences !== undefined) + for (const reference of parsed.projectReferences) + yield* this.createPrograms(this.checkConfigDirectory(reference.path), host, seen, true, isFileIncluded); } } +function getOutputsOfProjectReferences(program: ts.Program, host: ProjectHost) { + const references = program.getProjectReferences && program.getProjectReferences(); + if (references === undefined) + return []; + const seen = new Set(); + const result = []; + const moreReferences = []; + for (const ref of references) { + if (ref === undefined || seen.has(ref.sourceFile.fileName)) + continue; + seen.add(ref.sourceFile.fileName); + result.push(...getOutputFileNamesOfProjectReference(path.dirname(ref.sourceFile.fileName), ref.commandLine)); + if (ref.commandLine.projectReferences !== undefined) + moreReferences.push(...ref.commandLine.projectReferences); + } + for (const ref of moreReferences) + result.push(...getOutputFileNamesOfProjectReferenceRecursive(ref, seen, host)); + return result; +} + +/** recurse into every transitive project reference to exclude all of their outputs from linting */ +function getOutputFileNamesOfProjectReferenceRecursive(reference: ts.ProjectReference, seen: Set, host: ProjectHost) { + const referencePath = ts.resolveProjectReferencePath(host, reference); + if (seen.has(referencePath)) + return []; + const sourceFile = host.getSourceFile(referencePath, ts.ScriptTarget.JSON); + if (sourceFile === undefined) + return []; + const projectDirectory = path.dirname(referencePath); + const commandLine = ts.parseJsonSourceFileConfigFileContent( + sourceFile, + createParseConfigHost(host), + projectDirectory, + undefined, + referencePath, + ); + const result = getOutputFileNamesOfProjectReference(projectDirectory, commandLine); + if (commandLine.projectReferences !== undefined) + for (const ref of commandLine.projectReferences) + result.push(...getOutputFileNamesOfProjectReferenceRecursive(ref, seen, host)); + return result; +} + +function getOutputFileNamesOfProjectReference(projectDirectory: string, commandLine: ts.ParsedCommandLine) { + const options = commandLine.options; + if (options.outFile) + return [getOutFileDeclarationName(options.outFile)]; + return mapDefined(commandLine.fileNames, (fileName) => getDeclarationOutputName(fileName, options, projectDirectory)); +} + +function getDeclarationOutputName(fileName: string, options: ts.CompilerOptions, projectDirectory: string) { + const extension = path.extname(fileName); + switch (extension) { + case '.tsx': + break; + case '.ts': + if (path.extname(fileName.slice(0, -extension.length)) !== '.d') + break; + // falls through: .d.ts files produce no output + default: + return; + } + fileName = fileName.slice(0, -extension.length) + '.d.ts'; + return unixifyPath( + path.resolve( + options.declarationDir || options.outDir || projectDirectory, + path.relative(options.rootDir || projectDirectory, fileName), + ), + ); +} + +function getOutFileDeclarationName(outFile: string) { + // outFile ignores declarationDir + return outFile.slice(0, -path.extname(outFile).length) + '.d.ts'; +} + function getFiles(patterns: string[], exclude: string[], cwd: string): Iterable { const result: string[] = []; const globOptions = { @@ -290,7 +411,7 @@ function ensurePatternsMatch(include: IMinimatch[], exclude: IMinimatch[], files if (!glob.hasMagic(pattern.pattern)) { const normalized = pattern.set[0].join('/'); if (!files.includes(normalized) && !isExcluded(normalized, exclude)) - throw new ConfigurationError(`'${normalized}' is not included in the project.`); + throw new ConfigurationError(`'${normalized}' is not included in any of the projects.`); } } } @@ -338,9 +459,9 @@ declare module 'typescript' { function createParseConfigHost(host: ProjectHost): ts.ParseConfigHost { return { - useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), readDirectory(rootDir, extensions, excludes, includes, depth) { - return ts.matchFiles(rootDir, extensions, excludes, includes, ts.sys.useCaseSensitiveFileNames, host.cwd, depth, getEntries); + return host.readDirectory(rootDir, extensions, excludes, includes, depth); }, fileExists(f) { return host.fileExists(f); @@ -349,8 +470,4 @@ function createParseConfigHost(host: ProjectHost): ts.ParseConfigHost { return host.readFile(f); }, }; - - function getEntries(dir: string) { - return host.getDirectoryEntries(dir); - } } diff --git a/packages/wotan/src/utils.ts b/packages/wotan/src/utils.ts index 4f72dd70a..75d39f12e 100644 --- a/packages/wotan/src/utils.ts +++ b/packages/wotan/src/utils.ts @@ -64,12 +64,7 @@ function convertToPrintable(value: any): any { value = obj; } if (Array.isArray(value)) { - const result = []; - for (const element of value) { - const converted = convertToPrintable(element); - if (converted !== undefined) - result.push(converted); - } + const result = mapDefined(value, convertToPrintable); return result.length === 0 ? undefined : result; } const keys = Object.keys(value); @@ -119,3 +114,13 @@ export function hasSupportedExtension(fileName: string, extensions?: ReadonlyArr const ext = path.extname(fileName); return /^\.[jt]sx?$/.test(ext) || extensions !== undefined && extensions.includes(ext); } + +export function mapDefined(input: Iterable, cb: (item: T) => U | undefined) { + const result = []; + for (const item of input) { + const current = cb(item); + if (current !== undefined) + result.push(current); + } + return result; +} diff --git a/packages/wotan/test/argparse.spec.ts b/packages/wotan/test/argparse.spec.ts index 165ab4a88..467527660 100644 --- a/packages/wotan/test/argparse.spec.ts +++ b/packages/wotan/test/argparse.spec.ts @@ -12,6 +12,7 @@ test('parseGlobalOptions', (t) => { files: [], exclude: [], project: undefined, + references: false, formatter: undefined, fix: false, extensions: undefined, @@ -26,6 +27,7 @@ test('parseGlobalOptions', (t) => { files: [], exclude: [], project: undefined, + references: false, formatter: undefined, fix: false, extensions: [], @@ -41,6 +43,7 @@ test('parseGlobalOptions', (t) => { files: ['**/*.ts'], exclude: [], project: undefined, + references: false, formatter: 'foo', fix: 10, extensions: ['.mjs'], @@ -48,13 +51,14 @@ test('parseGlobalOptions', (t) => { ); t.deepEqual( - parseGlobalOptions({modules: [], config: 'config.yaml', project: '.', fix: true, exclude: '**/*.d.ts'}), + parseGlobalOptions({modules: [], config: 'config.yaml', project: '.', references: true, fix: true, exclude: '**/*.d.ts'}), { modules: [], config: 'config.yaml', files: [], exclude: ['**/*.d.ts'], project: '.', + references: true, formatter: undefined, fix: true, extensions: [], @@ -62,13 +66,14 @@ test('parseGlobalOptions', (t) => { ); t.deepEqual( - parseGlobalOptions({fix: 'foo', project: false, modules: [1]}), + parseGlobalOptions({fix: 'foo', project: false, references: 'false', modules: [1]}), { modules: [], config: undefined, files: [], exclude: [], project: undefined, + references: false, formatter: undefined, fix: false, extensions: [], @@ -88,6 +93,7 @@ test('defaults to lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -102,6 +108,7 @@ test('defaults to lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -119,6 +126,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -135,6 +143,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -151,6 +160,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: true, extensions: undefined, }, @@ -167,6 +177,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: '.', + references: false, fix: true, extensions: undefined, }, @@ -183,6 +194,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: '.', + references: false, fix: false, extensions: undefined, }, @@ -199,6 +211,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: true, extensions: undefined, }, @@ -215,6 +228,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: '.', + references: false, fix: 10, extensions: undefined, }, @@ -231,6 +245,7 @@ test('parses lint command', (t) => { exclude: ['**/*.d.ts', 'node_modules/**'], formatter: 'json', project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -247,6 +262,7 @@ test('parses lint command', (t) => { exclude: [], formatter: 'stylish', project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -263,6 +279,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -279,6 +296,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: undefined, }, @@ -295,6 +313,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: ['.mjs', '.es6', '.esm'], }, @@ -311,6 +330,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: ['.mjs', '.es6'], }, @@ -327,19 +347,55 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: false, extensions: ['.esm', '.mjs', '.es6'], }, '--ext merges arrays', ); + t.deepEqual( + parseArguments(['lint', '-r']), + { + command: CommandName.Lint, + modules: [], + config: undefined, + files: [], + exclude: [], + formatter: undefined, + project: undefined, + references: true, + fix: false, + extensions: undefined, + }, + '-r switches project references', + ); + + t.deepEqual( + parseArguments(['lint', '-r', '--references', 'false']), + { + command: CommandName.Lint, + modules: [], + config: undefined, + files: [], + exclude: [], + formatter: undefined, + project: undefined, + references: false, + fix: false, + extensions: undefined, + }, + '--references switches project references', + ); + t.deepEqual( parseArguments( - ['lint', '--ext', '', '-f', '', '-p', '', '-m', '', '-c', '', '-e', '', '--'], + ['lint', '--ext', '', '-f', '', '-p', '', '-m', '', '-c', '', '-e', '', '-r', 'false', '--'], { formatter: 'foo', extensions: 'bar', project: 'baz', + references: true, files: ['bas'], modules: ['foo', 'bar'], config: 'fooconfig', @@ -355,6 +411,7 @@ test('parses lint command', (t) => { exclude: [], formatter: undefined, project: undefined, + references: false, fix: true, extensions: undefined, }, @@ -368,6 +425,7 @@ test('parses lint command', (t) => { formatter: 'foo', extensions: 'bar', project: 'baz', + references: true, files: ['bas'], modules: ['foo', 'bar'], config: 'fooconfig', @@ -383,6 +441,7 @@ test('parses lint command', (t) => { exclude: ['**/*.d.ts'], formatter: 'foo', project: 'baz', + references: true, fix: 10, extensions: undefined, }, @@ -409,6 +468,7 @@ test('parses save command', (t) => { formatter: 'foo', extensions: 'bar', project: 'baz', + references: true, files: ['bas'], modules: ['foo', 'bar'], config: 'fooconfig', @@ -424,6 +484,7 @@ test('parses save command', (t) => { exclude: ['**/*.d.ts'], formatter: 'foo', project: 'baz', + references: true, fix: 10, extensions: undefined, }, diff --git a/packages/wotan/test/commands.spec.ts b/packages/wotan/test/commands.spec.ts index c79be60d0..dd5155687 100644 --- a/packages/wotan/test/commands.spec.ts +++ b/packages/wotan/test/commands.spec.ts @@ -156,6 +156,7 @@ test('SaveCommand', async (t) => { await verify({ command: CommandName.Save, project: undefined, + references: false, config: undefined, fix: false, exclude: [], @@ -175,6 +176,7 @@ test('SaveCommand', async (t) => { { command: CommandName.Save, project: undefined, + references: false, config: undefined, fix: 0, exclude: [], @@ -199,6 +201,7 @@ test('SaveCommand', async (t) => { { command: CommandName.Save, project: undefined, + references: false, config: '.wotanrc.yaml', fix: true, exclude: [], @@ -209,6 +212,7 @@ test('SaveCommand', async (t) => { }, { project: 'foo.json', + references: true, modules: ['foo', 'bar'], }, ), @@ -223,6 +227,7 @@ test('SaveCommand', async (t) => { { command: CommandName.Save, project: undefined, + references: false, config: undefined, fix: 0, exclude: [], @@ -328,6 +333,7 @@ test('LintCommand', async (t) => { exclude: [], config: '.wotanrc.yaml', project: undefined, + references: false, formatter: undefined, fix: true, extensions: undefined, @@ -348,6 +354,7 @@ test('LintCommand', async (t) => { exclude: [], config: '.wotanrc.fail.yaml', project: undefined, + references: false, formatter: undefined, fix: true, extensions: undefined, @@ -378,6 +385,7 @@ ERROR 2:8 no-unused-expression This expression is unused. Did you mean to assi exclude: [], config: '.wotanrc.fail-fix.yaml', project: undefined, + references: false, formatter: undefined, fix: true, extensions: undefined, @@ -402,6 +410,7 @@ ERROR 2:8 no-unused-expression This expression is unused. Did you mean to assi exclude: [], config: '.wotanrc.fail-fix.yaml', project: undefined, + references: false, formatter: undefined, fix: false, extensions: undefined, diff --git a/packages/wotan/test/project/references/.wotanrc.yaml b/packages/wotan/test/project/references/.wotanrc.yaml new file mode 100644 index 000000000..e48bc640a --- /dev/null +++ b/packages/wotan/test/project/references/.wotanrc.yaml @@ -0,0 +1,5 @@ +rulesDirectories: + local: '.' +rules: + local/quotemark: error + no-debugger: error diff --git a/packages/wotan/test/project/references/a/a.ts b/packages/wotan/test/project/references/a/a.ts new file mode 100644 index 000000000..5efe451ac --- /dev/null +++ b/packages/wotan/test/project/references/a/a.ts @@ -0,0 +1,3 @@ +export class A {} +// this fixable failure ensures we only lint this project / file once +debugger; diff --git a/packages/wotan/test/project/references/a/ambient.d.ts b/packages/wotan/test/project/references/a/ambient.d.ts new file mode 100644 index 000000000..901af585c --- /dev/null +++ b/packages/wotan/test/project/references/a/ambient.d.ts @@ -0,0 +1 @@ +declare var someGlobal: "a"; diff --git a/packages/wotan/test/project/references/a/index.ts b/packages/wotan/test/project/references/a/index.ts new file mode 100644 index 000000000..378dcf843 --- /dev/null +++ b/packages/wotan/test/project/references/a/index.ts @@ -0,0 +1 @@ +export * from "./a"; diff --git a/packages/wotan/test/project/references/a/tsconfig.json b/packages/wotan/test/project/references/a/tsconfig.json new file mode 100644 index 000000000..61a7f5f01 --- /dev/null +++ b/packages/wotan/test/project/references/a/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "outDir": "../out-a", + "declarationDir": "../outdir/a" + } +} diff --git a/packages/wotan/test/project/references/b/index.tsx b/packages/wotan/test/project/references/b/index.tsx new file mode 100644 index 000000000..1e282802a --- /dev/null +++ b/packages/wotan/test/project/references/b/index.tsx @@ -0,0 +1,3 @@ +import { A } from "../a"; + +export class B extends A {} diff --git a/packages/wotan/test/project/references/b/tsconfig.json b/packages/wotan/test/project/references/b/tsconfig.json new file mode 100644 index 000000000..b1f1a454b --- /dev/null +++ b/packages/wotan/test/project/references/b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "outDir": "../outdir/b" + }, + "files": ["index.tsx"], + "references": [ + {"path": "../a"} + ] +} diff --git a/packages/wotan/test/project/references/c/index.ts b/packages/wotan/test/project/references/c/index.ts new file mode 100644 index 000000000..020207756 --- /dev/null +++ b/packages/wotan/test/project/references/c/index.ts @@ -0,0 +1,6 @@ +import { B } from "../b"; +import { A } from "../a"; + +export class C extends B { + prop = new A(); +} diff --git a/packages/wotan/test/project/references/c/tsconfig.json b/packages/wotan/test/project/references/c/tsconfig.json new file mode 100644 index 000000000..32e2b7f6d --- /dev/null +++ b/packages/wotan/test/project/references/c/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "../outdir/c", + "composite": true + }, + "references": [ + {"path": "../b"}, + {"path": "../a"} + ] +} diff --git a/packages/wotan/test/project/references/d/index.ts b/packages/wotan/test/project/references/d/index.ts new file mode 100644 index 000000000..6aadde2c7 --- /dev/null +++ b/packages/wotan/test/project/references/d/index.ts @@ -0,0 +1,3 @@ +import { C } from "../c"; + +export class D extends C {} diff --git a/packages/wotan/test/project/references/d/tsconfig.json b/packages/wotan/test/project/references/d/tsconfig.json new file mode 100644 index 000000000..45162627f --- /dev/null +++ b/packages/wotan/test/project/references/d/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../outdir/d", + "composite": true + }, + "references": [ + {"path": "../c"} + ] +} diff --git a/packages/wotan/test/project/references/default.test.json b/packages/wotan/test/project/references/default.test.json new file mode 100644 index 000000000..546c7f265 --- /dev/null +++ b/packages/wotan/test/project/references/default.test.json @@ -0,0 +1,5 @@ +{ + "project": "e", + "references": false, + "typescriptVersion": ">= 3.0.0" +} diff --git a/packages/wotan/test/project/references/e/index.ts b/packages/wotan/test/project/references/e/index.ts new file mode 100644 index 000000000..439e1c35b --- /dev/null +++ b/packages/wotan/test/project/references/e/index.ts @@ -0,0 +1,3 @@ +import { D } from "../d"; + +export class E extends D {} diff --git a/packages/wotan/test/project/references/e/tsconfig.json b/packages/wotan/test/project/references/e/tsconfig.json new file mode 100644 index 000000000..ccbd8fd08 --- /dev/null +++ b/packages/wotan/test/project/references/e/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../outdir/e" + }, + "references": [ + {"path": "../d"}, + {"path": "../d/tsconfig.json"} + ] +} diff --git a/packages/wotan/test/project/references/filtered.test.json b/packages/wotan/test/project/references/filtered.test.json new file mode 100644 index 000000000..186cf804b --- /dev/null +++ b/packages/wotan/test/project/references/filtered.test.json @@ -0,0 +1,7 @@ +{ + "project": ".", + "references": true, + "exclude": ["{a,b,d}/index.ts?(x)", "outfile/**"], + "fix": false, + "typescriptVersion": ">= 3.0.0" +} diff --git a/packages/wotan/test/project/references/missing.test.json b/packages/wotan/test/project/references/missing.test.json new file mode 100644 index 000000000..31ce0d92f --- /dev/null +++ b/packages/wotan/test/project/references/missing.test.json @@ -0,0 +1,4 @@ +{ + "project": "missing/tsconfig.y.json", + "typescriptVersion": ">= 3.0.0" +} diff --git a/packages/wotan/test/project/references/missing/.wotanrc.yaml b/packages/wotan/test/project/references/missing/.wotanrc.yaml new file mode 100644 index 000000000..170b9ed49 --- /dev/null +++ b/packages/wotan/test/project/references/missing/.wotanrc.yaml @@ -0,0 +1,3 @@ +extends: ../.wotanrc.yaml +rules: + no-useless-assertion: error diff --git a/packages/wotan/test/project/references/missing/tsconfig.x.json b/packages/wotan/test/project/references/missing/tsconfig.x.json new file mode 100644 index 000000000..6dc589d1c --- /dev/null +++ b/packages/wotan/test/project/references/missing/tsconfig.x.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + }, + "files": [ + "x.ts" + ], + "references": [ + {"path": "tsconfig.missing.json"} + ] +} diff --git a/packages/wotan/test/project/references/missing/tsconfig.y.json b/packages/wotan/test/project/references/missing/tsconfig.y.json new file mode 100644 index 000000000..9f93acd8d --- /dev/null +++ b/packages/wotan/test/project/references/missing/tsconfig.y.json @@ -0,0 +1,9 @@ +{ + "files": [ + "y.ts" + ], + "references": [ + {"path": "tsconfig.x.json"}, + {"path": "tsconfig.missing.json"} + ] +} diff --git a/packages/wotan/test/project/references/missing/x.d.ts b/packages/wotan/test/project/references/missing/x.d.ts new file mode 100644 index 000000000..2b632022b --- /dev/null +++ b/packages/wotan/test/project/references/missing/x.d.ts @@ -0,0 +1,4 @@ +export declare class X { + // this file intentionally differs from 'x.ts' + prop: string; +} diff --git a/packages/wotan/test/project/references/missing/x.js b/packages/wotan/test/project/references/missing/x.js new file mode 100644 index 000000000..5b7e57d03 --- /dev/null +++ b/packages/wotan/test/project/references/missing/x.js @@ -0,0 +1,9 @@ +"use strict"; +exports.__esModule = true; +var X = /** @class */ (function () { + function X() { + this.prop = "x"; + } + return X; +}()); +exports.X = X; diff --git a/packages/wotan/test/project/references/missing/x.ts b/packages/wotan/test/project/references/missing/x.ts new file mode 100644 index 000000000..018b066a1 --- /dev/null +++ b/packages/wotan/test/project/references/missing/x.ts @@ -0,0 +1,3 @@ +export class X { + readonly prop = "x"; +} diff --git a/packages/wotan/test/project/references/missing/y.ts b/packages/wotan/test/project/references/missing/y.ts new file mode 100644 index 000000000..1f3c6f0f5 --- /dev/null +++ b/packages/wotan/test/project/references/missing/y.ts @@ -0,0 +1,4 @@ +import { X } from "./x"; + +// this needs two fixer runs and ensures it still resolves to 'x.d.ts' instead of 'x.ts' +new X().prop; diff --git a/packages/wotan/test/project/references/out-a/a.js b/packages/wotan/test/project/references/out-a/a.js new file mode 100644 index 000000000..e6c41d0f6 --- /dev/null +++ b/packages/wotan/test/project/references/out-a/a.js @@ -0,0 +1,10 @@ +"use strict"; +exports.__esModule = true; +var A = /** @class */ (function () { + function A() { + } + return A; +}()); +exports.A = A; +// this fixable failure ensures we only lint this project / file once +debugger; diff --git a/packages/wotan/test/project/references/out-a/index.js b/packages/wotan/test/project/references/out-a/index.js new file mode 100644 index 000000000..ec83f34a7 --- /dev/null +++ b/packages/wotan/test/project/references/out-a/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("./a")); diff --git a/packages/wotan/test/project/references/outdir/a/a.d.ts b/packages/wotan/test/project/references/outdir/a/a.d.ts new file mode 100644 index 000000000..5fd11f0c9 --- /dev/null +++ b/packages/wotan/test/project/references/outdir/a/a.d.ts @@ -0,0 +1,2 @@ +export declare class A { +} diff --git a/packages/wotan/test/project/references/outdir/a/index.d.ts b/packages/wotan/test/project/references/outdir/a/index.d.ts new file mode 100644 index 000000000..378dcf843 --- /dev/null +++ b/packages/wotan/test/project/references/outdir/a/index.d.ts @@ -0,0 +1 @@ +export * from "./a"; diff --git a/packages/wotan/test/project/references/outdir/b/index.d.ts b/packages/wotan/test/project/references/outdir/b/index.d.ts new file mode 100644 index 000000000..8524c50d9 --- /dev/null +++ b/packages/wotan/test/project/references/outdir/b/index.d.ts @@ -0,0 +1,3 @@ +import { A } from "../a"; +export declare class B extends A { +} diff --git a/packages/wotan/test/project/references/outdir/b/index.js b/packages/wotan/test/project/references/outdir/b/index.js new file mode 100644 index 000000000..06c2d1429 --- /dev/null +++ b/packages/wotan/test/project/references/outdir/b/index.js @@ -0,0 +1,24 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +exports.__esModule = true; +var a_1 = require("../a"); +var B = /** @class */ (function (_super) { + __extends(B, _super); + function B() { + return _super !== null && _super.apply(this, arguments) || this; + } + return B; +}(a_1.A)); +exports.B = B; diff --git a/packages/wotan/test/project/references/outdir/c/index.d.ts b/packages/wotan/test/project/references/outdir/c/index.d.ts new file mode 100644 index 000000000..78bb38a7e --- /dev/null +++ b/packages/wotan/test/project/references/outdir/c/index.d.ts @@ -0,0 +1,5 @@ +import { B } from "../b"; +import { A } from "../a"; +export declare class C extends B { + prop: A; +} diff --git a/packages/wotan/test/project/references/outdir/c/index.js b/packages/wotan/test/project/references/outdir/c/index.js new file mode 100644 index 000000000..d6abaa1bb --- /dev/null +++ b/packages/wotan/test/project/references/outdir/c/index.js @@ -0,0 +1,27 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +exports.__esModule = true; +var b_1 = require("../b"); +var a_1 = require("../a"); +var C = /** @class */ (function (_super) { + __extends(C, _super); + function C() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.prop = new a_1.A(); + return _this; + } + return C; +}(b_1.B)); +exports.C = C; diff --git a/packages/wotan/test/project/references/outdir/d/index.d.ts b/packages/wotan/test/project/references/outdir/d/index.d.ts new file mode 100644 index 000000000..eef8d5cfa --- /dev/null +++ b/packages/wotan/test/project/references/outdir/d/index.d.ts @@ -0,0 +1,3 @@ +import { C } from "../c"; +export declare class D extends C { +} diff --git a/packages/wotan/test/project/references/outdir/d/index.js b/packages/wotan/test/project/references/outdir/d/index.js new file mode 100644 index 000000000..86bc6c58e --- /dev/null +++ b/packages/wotan/test/project/references/outdir/d/index.js @@ -0,0 +1,24 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +exports.__esModule = true; +var c_1 = require("../c"); +var D = /** @class */ (function (_super) { + __extends(D, _super); + function D() { + return _super !== null && _super.apply(this, arguments) || this; + } + return D; +}(c_1.C)); +exports.D = D; diff --git a/packages/wotan/test/project/references/outfile.test.json b/packages/wotan/test/project/references/outfile.test.json new file mode 100644 index 000000000..27917b6f2 --- /dev/null +++ b/packages/wotan/test/project/references/outfile.test.json @@ -0,0 +1,3 @@ +{ + "project": "outfile/tsconfig.b.json" +} diff --git a/packages/wotan/test/project/references/outfile/a.ts b/packages/wotan/test/project/references/outfile/a.ts new file mode 100644 index 000000000..fcb8e0b59 --- /dev/null +++ b/packages/wotan/test/project/references/outfile/a.ts @@ -0,0 +1,3 @@ +namespace foo { + export const a = "a"; +} diff --git a/packages/wotan/test/project/references/outfile/b.ts b/packages/wotan/test/project/references/outfile/b.ts new file mode 100644 index 000000000..574516bd1 --- /dev/null +++ b/packages/wotan/test/project/references/outfile/b.ts @@ -0,0 +1,3 @@ +namespace foo { + console.log("a:", a); +} diff --git a/packages/wotan/test/project/references/outfile/out-a.d.ts b/packages/wotan/test/project/references/outfile/out-a.d.ts new file mode 100644 index 000000000..026d24b11 --- /dev/null +++ b/packages/wotan/test/project/references/outfile/out-a.d.ts @@ -0,0 +1,3 @@ +declare namespace foo { + const a = "a"; +} diff --git a/packages/wotan/test/project/references/outfile/out-a.js b/packages/wotan/test/project/references/outfile/out-a.js new file mode 100644 index 000000000..1210aff1c --- /dev/null +++ b/packages/wotan/test/project/references/outfile/out-a.js @@ -0,0 +1,4 @@ +var foo; +(function (foo) { + foo.a = "a"; +})(foo || (foo = {})); diff --git a/packages/wotan/test/project/references/outfile/tsconfig.a.json b/packages/wotan/test/project/references/outfile/tsconfig.a.json new file mode 100644 index 000000000..09a4bf0ed --- /dev/null +++ b/packages/wotan/test/project/references/outfile/tsconfig.a.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outFile": "./out-a.js", + "composite": true + }, + "files": [ + "a.ts" + ] +} diff --git a/packages/wotan/test/project/references/outfile/tsconfig.b.json b/packages/wotan/test/project/references/outfile/tsconfig.b.json new file mode 100644 index 000000000..36d618917 --- /dev/null +++ b/packages/wotan/test/project/references/outfile/tsconfig.b.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outFile": "./out-b.js" + }, + "files": [ + "b.ts" + ], + "references": [ + {"path": "./tsconfig.a.json", "prepend": true} + ] +} diff --git a/packages/wotan/test/project/references/quotemark.js b/packages/wotan/test/project/references/quotemark.js new file mode 100644 index 000000000..fbf56a82d --- /dev/null +++ b/packages/wotan/test/project/references/quotemark.js @@ -0,0 +1,11 @@ +// @ts-check +const AbstractRule = require('@fimbul/ymir').AbstractRule; +const ts = require('typescript'); + +exports.Rule = class Rule extends AbstractRule { + apply() { + for (const node of this.context.getFlatAst()) + if (node.kind === ts.SyntaxKind.StringLiteral && this.sourceFile.text[node.end - 1] === '"') + this.addFailureAtNode(node, 'Prefer single quotes'); + } +} diff --git a/packages/wotan/test/project/references/references.test.json b/packages/wotan/test/project/references/references.test.json new file mode 100644 index 000000000..c790c89a9 --- /dev/null +++ b/packages/wotan/test/project/references/references.test.json @@ -0,0 +1,4 @@ +{ + "references": true, + "typescriptVersion": ">= 3.0.0" +} diff --git a/packages/wotan/test/project/references/tsconfig.json b/packages/wotan/test/project/references/tsconfig.json new file mode 100644 index 000000000..6db3ad7d2 --- /dev/null +++ b/packages/wotan/test/project/references/tsconfig.json @@ -0,0 +1,12 @@ +{ + "files": [], + "references": [ + {"path": "a"}, + {"path": "b"}, + {"path": "c"}, + {"path": "d"}, + {"path": "e"}, + {"path": "a/tsconfig.json"}, + {"path": "outfile/tsconfig.b.json"} + ] +} diff --git a/packages/wotan/test/runner.spec.ts b/packages/wotan/test/runner.spec.ts index 82ed9b4a2..3455ba1a0 100644 --- a/packages/wotan/test/runner.spec.ts +++ b/packages/wotan/test/runner.spec.ts @@ -28,6 +28,7 @@ test('throws error on non-existing file', (t) => { ], exclude: ['*.js'], project: undefined, + references: false, fix: false, extensions: undefined, })), @@ -50,10 +51,11 @@ test('throws error on file not included in project', (t) => { ], exclude: ['*.js'], project: 'test/project/setup', + references: false, fix: false, extensions: undefined, })), - `'${unixifyPath(path.resolve('packages/wotan/non-existent.ts'))}' is not included in the project.`, + `'${unixifyPath(path.resolve('packages/wotan/non-existent.ts'))}' is not included in any of the projects.`, ); }); @@ -82,6 +84,7 @@ test('throws if no tsconfig.json can be found', (t) => { files: [], exclude: [], project: root, + references: false, fix: false, extensions: undefined, })), @@ -95,6 +98,7 @@ test('throws if no tsconfig.json can be found', (t) => { files: [], exclude: [], project: dir, + references: false, fix: false, extensions: undefined, })), @@ -107,6 +111,7 @@ test('throws if no tsconfig.json can be found', (t) => { files: [], exclude: [], project: undefined, + references: false, fix: false, extensions: undefined, })), @@ -163,6 +168,7 @@ test('reports warnings while parsing tsconfig.json', (t) => { files: [], exclude: [], project: 'invalid-config.json', + references: false, fix: false, extensions: undefined, })); @@ -174,6 +180,7 @@ test('reports warnings while parsing tsconfig.json', (t) => { files: [], exclude: [], project: 'invalid-base.json', + references: false, fix: false, extensions: undefined, })); @@ -185,6 +192,7 @@ test('reports warnings while parsing tsconfig.json', (t) => { files: [], exclude: [], project: 'invalid-files.json', + references: false, fix: false, extensions: undefined, })); @@ -196,6 +204,7 @@ test('reports warnings while parsing tsconfig.json', (t) => { files: [], exclude: [], project: 'no-match.json', + references: false, fix: false, extensions: undefined, })); @@ -305,6 +314,7 @@ test.skip('excludes symlinked typeRoots', (t) => { files: [], exclude: [], project: 'tsconfig.json', + references: false, fix: false, extensions: undefined, })); @@ -339,6 +349,7 @@ test('works with absolute and relative paths', (t) => { 'test/fixtures/paths/d.ts', ], project: project ? 'test/fixtures/paths/tsconfig.json' : undefined, + references: false, fix: false, extensions: undefined, }));