diff --git a/.eslintrc.js b/.eslintrc.js index 337e239983b8..20b6b1f86903 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,7 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/unbound-method': 'off', // diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b8fddd22bc..5f3790b06c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,37 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + + +### Bug Fixes + +* **eslint-plugin:** [no-dynamic-delete] correct invalid fixer for identifiers ([#1244](https://github.com/typescript-eslint/typescript-eslint/issues/1244)) ([5b1300d](https://github.com/typescript-eslint/typescript-eslint/commit/5b1300d)) +* **eslint-plugin:** [no-untyped-pub-sig] constructor return ([#1231](https://github.com/typescript-eslint/typescript-eslint/issues/1231)) ([6cfd468](https://github.com/typescript-eslint/typescript-eslint/commit/6cfd468)) +* **eslint-plugin:** [prefer-optional-chain] unhandled cases ([b1a065f](https://github.com/typescript-eslint/typescript-eslint/commit/b1a065f)) +* **eslint-plugin:** [req-await] crash on nonasync promise return ([#1228](https://github.com/typescript-eslint/typescript-eslint/issues/1228)) ([56c00b3](https://github.com/typescript-eslint/typescript-eslint/commit/56c00b3)) +* **typescript-estree:** fix synthetic default import ([#1245](https://github.com/typescript-eslint/typescript-eslint/issues/1245)) ([d97f809](https://github.com/typescript-eslint/typescript-eslint/commit/d97f809)) + + +### Features + +* **eslint-plugin:** [camelcase] add genericType option ([#925](https://github.com/typescript-eslint/typescript-eslint/issues/925)) ([d785c61](https://github.com/typescript-eslint/typescript-eslint/commit/d785c61)) +* **eslint-plugin:** [no-empty-interface] noEmptyWithSuper fixer ([#1247](https://github.com/typescript-eslint/typescript-eslint/issues/1247)) ([b91b0ba](https://github.com/typescript-eslint/typescript-eslint/commit/b91b0ba)) +* **eslint-plugin:** [no-extran-class] add allowWithDecorator opt ([#886](https://github.com/typescript-eslint/typescript-eslint/issues/886)) ([f1ab9a2](https://github.com/typescript-eslint/typescript-eslint/commit/f1ab9a2)) +* **eslint-plugin:** [no-unnece-cond] Add allowConstantLoopConditions ([#1029](https://github.com/typescript-eslint/typescript-eslint/issues/1029)) ([ceb6f1c](https://github.com/typescript-eslint/typescript-eslint/commit/ceb6f1c)) +* **eslint-plugin:** [restrict-plus-operands] check += ([#892](https://github.com/typescript-eslint/typescript-eslint/issues/892)) ([fa88cb9](https://github.com/typescript-eslint/typescript-eslint/commit/fa88cb9)) +* suggestion types, suggestions for no-explicit-any ([#1250](https://github.com/typescript-eslint/typescript-eslint/issues/1250)) ([b16a4b6](https://github.com/typescript-eslint/typescript-eslint/commit/b16a4b6)) +* **eslint-plugin:** add no-extra-non-null-assertion ([#1183](https://github.com/typescript-eslint/typescript-eslint/issues/1183)) ([2b3b5d6](https://github.com/typescript-eslint/typescript-eslint/commit/2b3b5d6)) +* **eslint-plugin:** add no-unused-vars-experimental ([#688](https://github.com/typescript-eslint/typescript-eslint/issues/688)) ([05ebea5](https://github.com/typescript-eslint/typescript-eslint/commit/05ebea5)) +* **eslint-plugin:** add prefer-nullish-coalescing ([#1069](https://github.com/typescript-eslint/typescript-eslint/issues/1069)) ([a9cd399](https://github.com/typescript-eslint/typescript-eslint/commit/a9cd399)) +* **eslint-plugin:** add return-await rule ([#1050](https://github.com/typescript-eslint/typescript-eslint/issues/1050)) ([0ff4620](https://github.com/typescript-eslint/typescript-eslint/commit/0ff4620)) +* **eslint-plugin:** add rule prefer-optional-chain ([#1213](https://github.com/typescript-eslint/typescript-eslint/issues/1213)) ([ad7e1a7](https://github.com/typescript-eslint/typescript-eslint/commit/ad7e1a7)) +* **eslint-plugin:** optional chain support in rules (part 1) ([#1253](https://github.com/typescript-eslint/typescript-eslint/issues/1253)) ([f5c0e02](https://github.com/typescript-eslint/typescript-eslint/commit/f5c0e02)) + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) diff --git a/README.md b/README.md index f296ee034eff..1f6d7c3cac1d 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ This is not valid JavaScript code, because it contains a so-called type annotati However, we can leverage the fact that ESLint has been designed with these use-cases in mind! -It turns out that ESLint is not just comprised of one library, instead it is comprised of a few important moving parts. One of those moving parts is **the parser**. ESLint ships with a parser built in (called [`espree`](https://github.com/eslint/espree)), and so if you only ever write standard JavaScript, you don't need to care about this implementation detail. +It turns out that ESLint is not just one library. Instead it is composed of a few important moving parts. One of those moving parts is **the parser**. ESLint ships with a parser built in (called [`espree`](https://github.com/eslint/espree)), and so if you only ever write standard JavaScript, you don't need to care about this implementation detail. The great thing is, though, if we want to support non-standard JavaScript syntax, all we need to do is provide ESLint with an alternative parser to use - that is a first-class use-case offered by ESLint. diff --git a/lerna.json b/lerna.json index 2eec75f4f591..3881b978c009 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.0", + "version": "2.9.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/package.json b/package.json index 111bf02de8ba..aca3b80aee45 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@types/node": "^12.12.7", "all-contributors-cli": "^6.11.0", "cz-conventional-changelog": "^3.0.2", - "eslint": "^6.6.0", + "eslint": "^6.7.0", "eslint-plugin-eslint-comments": "^3.1.2", "eslint-plugin-eslint-plugin": "^2.1.0", "eslint-plugin-import": "^2.18.2", diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 7b7467bd4579..634660606399 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 631e7d54ad4c..f9011094ab1b 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "2.8.0", + "version": "2.9.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -31,7 +31,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.8.0", + "@typescript-eslint/experimental-utils": "2.9.0", "lodash.memoize": "^4.1.2" }, "peerDependencies": { @@ -41,6 +41,6 @@ }, "devDependencies": { "@types/lodash.memoize": "^4.1.4", - "@typescript-eslint/parser": "2.8.0" + "@typescript-eslint/parser": "2.9.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 295a43be6cc8..8bd348828083 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,36 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + + +### Bug Fixes + +* **eslint-plugin:** [no-dynamic-delete] correct invalid fixer for identifiers ([#1244](https://github.com/typescript-eslint/typescript-eslint/issues/1244)) ([5b1300d](https://github.com/typescript-eslint/typescript-eslint/commit/5b1300d)) +* **eslint-plugin:** [no-untyped-pub-sig] constructor return ([#1231](https://github.com/typescript-eslint/typescript-eslint/issues/1231)) ([6cfd468](https://github.com/typescript-eslint/typescript-eslint/commit/6cfd468)) +* **eslint-plugin:** [prefer-optional-chain] unhandled cases ([b1a065f](https://github.com/typescript-eslint/typescript-eslint/commit/b1a065f)) +* **eslint-plugin:** [req-await] crash on nonasync promise return ([#1228](https://github.com/typescript-eslint/typescript-eslint/issues/1228)) ([56c00b3](https://github.com/typescript-eslint/typescript-eslint/commit/56c00b3)) + + +### Features + +* **eslint-plugin:** [camelcase] add genericType option ([#925](https://github.com/typescript-eslint/typescript-eslint/issues/925)) ([d785c61](https://github.com/typescript-eslint/typescript-eslint/commit/d785c61)) +* **eslint-plugin:** [no-empty-interface] noEmptyWithSuper fixer ([#1247](https://github.com/typescript-eslint/typescript-eslint/issues/1247)) ([b91b0ba](https://github.com/typescript-eslint/typescript-eslint/commit/b91b0ba)) +* **eslint-plugin:** [no-extran-class] add allowWithDecorator opt ([#886](https://github.com/typescript-eslint/typescript-eslint/issues/886)) ([f1ab9a2](https://github.com/typescript-eslint/typescript-eslint/commit/f1ab9a2)) +* **eslint-plugin:** [no-unnece-cond] Add allowConstantLoopConditions ([#1029](https://github.com/typescript-eslint/typescript-eslint/issues/1029)) ([ceb6f1c](https://github.com/typescript-eslint/typescript-eslint/commit/ceb6f1c)) +* **eslint-plugin:** [restrict-plus-operands] check += ([#892](https://github.com/typescript-eslint/typescript-eslint/issues/892)) ([fa88cb9](https://github.com/typescript-eslint/typescript-eslint/commit/fa88cb9)) +* suggestion types, suggestions for no-explicit-any ([#1250](https://github.com/typescript-eslint/typescript-eslint/issues/1250)) ([b16a4b6](https://github.com/typescript-eslint/typescript-eslint/commit/b16a4b6)) +* **eslint-plugin:** add no-extra-non-null-assertion ([#1183](https://github.com/typescript-eslint/typescript-eslint/issues/1183)) ([2b3b5d6](https://github.com/typescript-eslint/typescript-eslint/commit/2b3b5d6)) +* **eslint-plugin:** add no-unused-vars-experimental ([#688](https://github.com/typescript-eslint/typescript-eslint/issues/688)) ([05ebea5](https://github.com/typescript-eslint/typescript-eslint/commit/05ebea5)) +* **eslint-plugin:** add prefer-nullish-coalescing ([#1069](https://github.com/typescript-eslint/typescript-eslint/issues/1069)) ([a9cd399](https://github.com/typescript-eslint/typescript-eslint/commit/a9cd399)) +* **eslint-plugin:** add return-await rule ([#1050](https://github.com/typescript-eslint/typescript-eslint/issues/1050)) ([0ff4620](https://github.com/typescript-eslint/typescript-eslint/commit/0ff4620)) +* **eslint-plugin:** add rule prefer-optional-chain ([#1213](https://github.com/typescript-eslint/typescript-eslint/issues/1213)) ([ad7e1a7](https://github.com/typescript-eslint/typescript-eslint/commit/ad7e1a7)) +* **eslint-plugin:** optional chain support in rules (part 1) ([#1253](https://github.com/typescript-eslint/typescript-eslint/issues/1253)) ([f5c0e02](https://github.com/typescript-eslint/typescript-eslint/commit/f5c0e02)) + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 4a7557f6f46c..503c06e6894e 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -162,8 +162,9 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Bans usage of the delete operator with computed key expressions | | :wrench: | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | -| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | | +| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | | [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: | @@ -185,6 +186,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-untyped-public-signature`](./docs/rules/no-untyped-public-signature.md) | Requires that all public method arguments and return type will be explicitly typed | | | | | [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | +| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments. | | | :thought_balloon: | | [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | | [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | @@ -192,6 +194,8 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | | [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | :wrench: | | | [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | @@ -201,6 +205,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | +| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Rules for awaiting returned promises | | | :thought_balloon: | | [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | | [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :wrench: | | | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/camelcase.md b/packages/eslint-plugin/docs/rules/camelcase.md index 84c4d9d13b5d..5fa0c8e530ff 100644 --- a/packages/eslint-plugin/docs/rules/camelcase.md +++ b/packages/eslint-plugin/docs/rules/camelcase.md @@ -30,8 +30,10 @@ variable that will be imported into the local module scope. This rule has an object option: -- `"properties": "always"` (default) enforces camelcase style for property names -- `"properties": "never"` does not check property names +- `"properties": "never"` (default) does not check property names +- `"properties": "always"` enforces camelcase style for property names +- `"genericType": "never"` (default) does not check generic identifiers +- `"genericType": "always"` enforces camelcase style for generic identifiers - `"ignoreDestructuring": false` (default) enforces camelcase style for destructured identifiers - `"ignoreDestructuring": true` does not check destructured identifiers - `allow` (`string[]`) list of properties to accept. Accept regex. @@ -129,6 +131,100 @@ var obj = { }; ``` +### genericType: "always" + +Examples of **incorrect** code for this rule with the default `{ "genericType": "always" }` option: + +```typescript +/* eslint @typescript-eslint/camelcase: ["error", { "genericType": "always" }] */ + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} +``` + +Examples of **correct** code for this rule with the default `{ "genericType": "always" }` option: + +```typescript +/* eslint @typescript-eslint/camelcase: ["error", { "genericType": "always" }] */ + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} +``` + +### genericType: "never" + +Examples of **correct** code for this rule with the `{ "genericType": "never" }` option: + +```typescript +/* eslint @typescript-eslint/camelcase: ["error", { "genericType": "never" }] */ + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} + +interface Foo {} +function foo() {} +class Foo {} +type Foo = {}; +class Foo { + method() {} +} +``` + ### ignoreDestructuring: false Examples of **incorrect** code for this rule with the default `{ "ignoreDestructuring": false }` option: diff --git a/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md b/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md new file mode 100644 index 000000000000..37f4d2e3a52f --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md @@ -0,0 +1,37 @@ +# Disallow extra non-null assertion + +## Rule Details + +Examples of **incorrect** code for this rule: + +```ts +const foo: { bar: number } | null = null; +const bar = foo!!!.bar; +``` + +```ts +function foo(bar: number | undefined) { + const bar: number = bar!!!; +} +``` + +Examples of **correct** code for this rule: + +```ts +const foo: { bar: number } | null = null; +const bar = foo!.bar; +``` + +```ts +function foo(bar: number | undefined) { + const bar: number = bar!; +} +``` + +## How to use + +```json +{ + "@typescript-eslint/no-extra-non-null-assertion": ["error"] +} +``` diff --git a/packages/eslint-plugin/docs/rules/no-extraneous-class.md b/packages/eslint-plugin/docs/rules/no-extraneous-class.md index 882d7e26a554..3c1fa0528351 100644 --- a/packages/eslint-plugin/docs/rules/no-extraneous-class.md +++ b/packages/eslint-plugin/docs/rules/no-extraneous-class.md @@ -50,9 +50,25 @@ const StaticOnly = { This rule accepts a single object option. -- `allowConstructorOnly: true` will silence warnings about classes containing only a constructor. -- `allowEmpty: true` will silence warnings about empty classes. -- `allowStaticOnly: true` will silence warnings about classes containing only static members. +```ts +type Options = { + // allow extraneous classes if they only contain a constructor + allowConstructorOnly?: boolean; + // allow extraneous classes if they have no body (i.e. are empty) + allowEmpty?: boolean; + // allow extraneous classes if they only contain static members + allowStaticOnly?: boolean; + // allow extraneous classes if they are have a decorator + allowWithDecorator?: boolean; +}; + +const defaultOptions: Options = { + allowConstructorOnly: false, + allowEmpty: false, + allowStaticOnly: false, + allowWithDecorator: false, +}; +``` ## When Not To Use It diff --git a/packages/eslint-plugin/docs/rules/no-this-alias.md b/packages/eslint-plugin/docs/rules/no-this-alias.md index b2c891fa0e6d..4a512be31eb8 100644 --- a/packages/eslint-plugin/docs/rules/no-this-alias.md +++ b/packages/eslint-plugin/docs/rules/no-this-alias.md @@ -1,6 +1,6 @@ # Disallow aliasing `this` (no-this-alias) -This rule prohibts assigning variables to `this`. +This rule prohibits assigning variables to `this`. ## Rule Details diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md index 48b98849c41e..4bc160e68ccd 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md @@ -53,6 +53,16 @@ function head(items: T[]) { } ``` +- `allowConstantLoopConditions` (default `false`) - allows constant expressions in loops. + +Example of correct code for when `allowConstantLoopConditions` is `true`: + +```ts +while (true) {} +for (; true; ) {} +do {} while (true); +``` + ## When Not To Use It The main downside to using this rule is the need for type information. diff --git a/packages/eslint-plugin/docs/rules/no-unused-expressions.md b/packages/eslint-plugin/docs/rules/no-unused-expressions.md index 7da998ab2c6c..e1a86449181e 100644 --- a/packages/eslint-plugin/docs/rules/no-unused-expressions.md +++ b/packages/eslint-plugin/docs/rules/no-unused-expressions.md @@ -1,4 +1,4 @@ -# require or disallow semicolons instead of ASI (semi) +# Disallow Unused Expressions (no-unused-expressions) This rule aims to eliminate unused expressions which have no effect on the state of the program. diff --git a/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md b/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md new file mode 100644 index 000000000000..7ee9a6d84324 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md @@ -0,0 +1,115 @@ +# Disallow unused variables and arguments (no-unused-vars-experimental) + +Variables that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. Such variables take up space in the code and can lead to confusion by readers. + +## Rule Details + +This rule leverages the TypeScript compiler's unused variable checks to report. This means that with all rule options set to `false`, it should report the same errors as if you used both the `noUnusedLocals` and `noUnusedParameters` compiler options. + +This rule is vastly different to, and maintains no compatability with the base eslint version of the rule. + +### Limitations + +There are two limitations to this rule when compared with eslint's `no-unused-vars` rule, which are imposed by the fact that it directly uses TypeScript's implementation. + +1. This rule only works on files that TypeScript deems is a module (i.e. it has an `import` or an `export` statement). +2. The rule is significantly less configurable, as it cannot deviate too far from the base implementation. + +## Supported Nodes + +This rule supports checks on the following features: + +- Declarations: + - `var` / `const` / `let` + - `function` + - `class` + - `enum` + - `interface` + - `type` +- Class methods +- Class properties and parameter properties +- Function parameters +- Generic type parameters +- Import statements + +## Options + +```ts +type Options = { + ignoredNamesRegex?: string | boolean; + ignoreArgsIfArgsAfterAreUsed?: boolean; +}; + +const defaultOptions: Options = { + ignoredNamesRegex: '^_', + ignoreArgsIfArgsAfterAreUsed: false, +}; +``` + +### ignoredNamesRegex + +This option accepts a regex string to match names against. +Any matched names will be ignored and have no errors reported. +If you set it to false, it will never ignore any names. + +The default value is `'^_'` (i.e. matches any name prefixed with an underscore). + +Examples of valid code with `{ variables: { ignoredNamesRegex: '^_' } }`. + +```ts +const _unusedVar = 'unused'; +class _Unused { + private _unused = 1; + private _unusedMethod() {} +} +function _unusedFunction() {} +enum _UnusedEnum { + a = 1, +} +interface _UnusedInterface {} +type _UnusedType = {}; +``` + +**_NOTE:_** The TypeScript compiler automatically ignores imports, function arguments, type parameter declarations, and object destructuring variables prefixed with an underscore. +As this is hard-coded into the compiler, we cannot change this. + +Examples of valid code based on the unchangable compiler settings + +```ts +import _UnusedDefault, { _UnusedNamed } from 'foo'; +export function foo(_unusedProp: string) {} +export class Foo<_UnusedGeneric> {} +const { prop: _unusedDesctructure } = foo; +``` + +## ignoreArgsIfArgsAfterAreUsed + +When true, this option will ignore unused function arguments if the arguments proceeding arguments are used. + +Examples of invalid code with `{ ignoreArgsIfArgsAfterAreUsed: false }` + +```ts +function foo(unused: string, used: number) { + console.log(used); +} + +class Foo { + constructor(unused: string, public used: number) { + console.log(used); + } +} +``` + +Examples of valid code with `{ ignoreArgsIfArgsAfterAreUsed: true }` + +```ts +function foo(unused: string, used: number) { + console.log(used); +} + +class Foo { + constructor(unused: string, public used: number) { + console.log(used); + } +} +``` diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md new file mode 100644 index 000000000000..140ade2dbe7c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -0,0 +1,141 @@ +# Enforce the usage of the nullish coalescing operator instead of logical chaining (prefer-nullish-coalescing) + +TypeScript 3.7 added support for the nullish coalescing operator. +This operator allows you to safely cascade a value when dealing with `null` or `undefined`. + +```ts +function myFunc(foo: string | null) { + return foo ?? 'a string'; +} + +// is equivalent to + +function myFunc(foo: string | null) { + return foo !== null && foo !== undefined ? foo : 'a string'; +} +``` + +Because the nullish coalescing operator _only_ coalesces when the original value is `null` or `undefined`, it is much safer than relying upon logical OR operator chaining `||`; which coalesces on any _falsey_ value: + +```ts +const emptyString = ''; + +const nullish1 = emptyString ?? 'unsafe'; +const logical1 = emptyString || 'unsafe'; + +// nullish1 === '' +// logical1 === 'unsafe' + +declare const nullString: string | null; + +const nullish2 = nullString ?? 'safe'; +const logical2 = nullString || 'safe'; + +// nullish2 === 'safe' +// logical2 === 'safe' +``` + +## Rule Details + +This rule aims enforce the usage of the safer operator. + +## Options + +```ts +type Options = [ + { + ignoreConditionalTests?: boolean; + ignoreMixedLogicalExpressions?: boolean; + }, +]; + +const defaultOptions = [ + { + ignoreConditionalTests: true, + ignoreMixedLogicalExpressions: true; + }, +]; +``` + +### ignoreConditionalTests + +Setting this option to `true` (the default) will cause the rule to ignore any cases that are located within a conditional test. + +Generally expressions within conditional tests intentionally use the falsey fallthrough behaviour of the logical or operator, meaning that fixing the operator to the nullish coalesce operator could cause bugs. + +If you're looking to enforce stricter conditional tests, you should consider using the `strict-boolean-expressions` rule. + +Incorrect code for `ignoreConditionalTests: false`, and correct code for `ignoreConditionalTests: true`: + +```ts +declare const a: string | null; +declare const b: string | null; + +if (a || b) { +} +while (a || b) {} +do {} while (a || b); +for (let i = 0; a || b; i += 1) {} +a || b ? true : false; +``` + +Correct code for `ignoreConditionalTests: false`: + +```ts +declare const a: string | null; +declare const b: string | null; + +if (a ?? b) { +} +while (a ?? b) {} +do {} while (a ?? b); +for (let i = 0; a ?? b; i += 1) {} +a ?? b ? true : false; +``` + +### ignoreMixedLogicalExpressions + +Setting this option to `true` (the default) will cause the rule to ignore any logical or expressions thare are part of a mixed logical expression (with `&&`). + +Generally expressions within mixed logical expressions intentionally use the falsey fallthrough behaviour of the logical or operator, meaning that fixing the operator to the nullish coalesce operator could cause bugs. + +If you're looking to enforce stricter conditional tests, you should consider using the `strict-boolean-expressions` rule. + +Incorrect code for `ignoreMixedLogicalExpressions: false`, and correct code for `ignoreMixedLogicalExpressions: true`: + +```ts +declare const a: string | null; +declare const b: string | null; +declare const c: string | null; +declare const d: string | null; + +a || (b && c); +(a && b) || c || d; +a || (b && c) || d; +a || (b && c && d); +``` + +Correct code for `ignoreMixedLogicalExpressions: false`: + +```ts +declare const a: string | null; +declare const b: string | null; +declare const c: string | null; +declare const d: string | null; + +a ?? (b && c); +(a && b) ?? c ?? d; +a ?? (b && c) ?? d; +a ?? (b && c && d); +``` + +**_NOTE:_** Errors for this specific case will be presented as suggestions, instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression. + +## When Not To Use It + +If you are not using TypeScript 3.7 (or greater), then you will not be able to use this rule, as the operator is not supported. + +## Further Reading + +- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) +- [Nullish Coalescing Operator Proposal](https://github.com/tc39/proposal-nullish-coalescing/) diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md new file mode 100644 index 000000000000..410db3c931aa --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md @@ -0,0 +1,80 @@ +# Prefer using concise optional chain expressions instead of chained logical ands (prefer-optional-chain) + +TypeScript 3.7 added support for the optional chain operator. +This operator allows you to safely access properties and methods on objects when they are potentially `null` or `undefined`. + +```ts +type T = { + a?: { + b?: { + c: string; + method?: () => void; + }; + }; +}; + +function myFunc(foo: T | null) { + return foo?.a?.b?.c; +} +// is roughly equivalent to +function myFunc(foo: T | null) { + return foo && foo.a && foo.a.b && foo.a.b.c; +} + +function myFunc(foo: T | null) { + return foo?.['a']?.b?.c; +} +// is roughly equivalent to +function myFunc(foo: T | null) { + return foo && foo['a'] && foo['a'].b && foo['a'].b.c; +} + +function myFunc(foo: T | null) { + return foo?.a?.b?.method?.(); +} +// is roughly equivalent to +function myFunc(foo: T | null) { + return foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method(); +} +``` + +Because the optional chain operator _only_ chains when the property value is `null` or `undefined`, it is much safer than relying upon logical AND operator chaining `&&`; which chains on any _truthy_ value. + +## Rule Details + +This rule aims enforce the usage of the safer operator. + +Examples of **incorrect** code for this rule: + +```ts +foo && foo.a && foo.a.b && foo.a.b.c; +foo && foo['a'] && foo['a'].b && foo['a'].b.c; +foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method(); + +// this rule also supports converting chained strict nullish checks: +foo && + foo.a != null && + foo.a.b !== null && + foo.a.b.c != undefined && + foo.a.b.c.d !== undefined && + foo.a.b.c.d.e; +``` + +Examples of **correct** code for this rule: + +```ts +foo?.a?.b?.c; +foo?.['a']?.b?.c; +foo?.a?.b?.method?.(); + +foo?.a?.b?.c?.d?.e; +``` + +## When Not To Use It + +If you are not using TypeScript 3.7 (or greater), then you will not be able to use this rule, as the operator is not supported. + +## Further Reading + +- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) +- [Optional Chaining Proposal](https://github.com/tc39/proposal-optional-chaining/) diff --git a/packages/eslint-plugin/docs/rules/restrict-plus-operands.md b/packages/eslint-plugin/docs/rules/restrict-plus-operands.md index 1efffa83e69e..7b5af55a84ff 100644 --- a/packages/eslint-plugin/docs/rules/restrict-plus-operands.md +++ b/packages/eslint-plugin/docs/rules/restrict-plus-operands.md @@ -16,6 +16,37 @@ var foo = 1n + 1; ## Options +This rule has an object option: + +- `"checkCompoundAssignments": false`: (default) does not check compound assignments (`+=`) +- `"checkCompoundAssignments": true` + +### checkCompoundAssignments + +Examples of **incorrect** code for the `{ "checkCompoundAssignments": true }` option: + +```ts +/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ + +let foo: string | undefined; +foo += 'some data'; + +let bar: string = ''; +bar += 0; +``` + +Examples of **correct** code for the `{ "checkCompoundAssignments": true }` option: + +```ts +/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ + +let foo: number = 0; +foo += 1; + +let bar = ''; +bar += 'test'; +``` + ```json { "@typescript-eslint/restrict-plus-operands": "error" diff --git a/packages/eslint-plugin/docs/rules/return-await.md b/packages/eslint-plugin/docs/rules/return-await.md new file mode 100644 index 000000000000..606b501a7d28 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/return-await.md @@ -0,0 +1,123 @@ +# Require/Disallow returning awaited values in specific contexts (@typescript-eslint/return-await) + +Returning an awaited promise can make sense for better stack trace information as well as for consistent error handling (returned promises will not be caught in an async function try/catch). + +## Rule Details + +The `@typescript-eslint/return-await` rule specifies that awaiting a returned non-promise is never allowed. By default, the rule requires awaiting a returned promise in a `try-catch-finally` block and disallows returning an awaited promise in any other context. Optionally, the rule can require awaiting returned promises in all contexts, or disallow them in all contexts. + +## Options + +`in-try-catch` (default): `await`-ing a returned promise is required in `try-catch-finally` blocks and disallowed elsewhere. + +`always`: `await`-ing a returned promise is required everywhere. + +`never`: `await`-ing a returned promise is disallowed everywhere. + +```typescript +// valid in-try-catch +async function validInTryCatch1() { + try { + return await Promise.resolve('try'); + } catch (e) {} +} + +async function validInTryCatch2() { + return Promise.resolve('try'); +} + +async function validInTryCatch3() { + return 'value'; +} + +// valid always +async function validAlways1() { + try { + return await Promise.resolve('try'); + } catch (e) {} +} + +async function validAlways2() { + return await Promise.resolve('try'); +} + +async function validAlways3() { + return 'value'; +} + +// valid never +async function validNever1() { + try { + return Promise.resolve('try'); + } catch (e) {} +} + +async function validNever2() { + return Promise.resolve('try'); +} + +async function validNever3() { + return 'value'; +} +``` + +```typescript +// invalid in-try-catch +async function invalidInTryCatch1() { + try { + return Promise.resolve('try'); + } catch (e) {} +} + +async function invalidInTryCatch2() { + return await Promise.resolve('try'); +} + +async function invalidInTryCatch3() { + return await 'value'; +} + +// invalid always +async function invalidAlways1() { + try { + return Promise.resolve('try'); + } catch (e) {} +} + +async function invalidAlways2() { + return Promise.resolve('try'); +} + +async function invalidAlways3() { + return await 'value'; +} + +// invalid never +async function invalidNever1() { + try { + return await Promise.resolve('try'); + } catch (e) {} +} + +async function invalidNever2() { + return await Promise.resolve('try'); +} + +async function invalidNever3() { + return await 'value'; +} +``` + +The rule also applies to `finally` blocks. So the following would be invalid with default options: + +```typescript +async function invalid() { + try { + return await Promise.resolve('try'); + } catch (e) { + return Promise.resolve('catch'); + } finally { + // cleanup + } +} +``` diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 0a84ab84ddbb..551199641903 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.8.0", + "version": "2.9.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -40,7 +40,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.8.0", + "@typescript-eslint/experimental-utils": "2.9.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/eslint-plugin/src/configs/README.md b/packages/eslint-plugin/src/configs/README.md index 95accb694903..28747608efb7 100644 --- a/packages/eslint-plugin/src/configs/README.md +++ b/packages/eslint-plugin/src/configs/README.md @@ -1,6 +1,6 @@ # Premade configs -These configs exist for your convenience. They contain configuration intended to save you time and effort when configuring your project by disabling rules known to conflict with this repository, or cause issues in typesript codebases. +These configs exist for your convenience. They contain configuration intended to save you time and effort when configuring your project by disabling rules known to conflict with this repository, or cause issues in TypeScript codebases. ## All @@ -26,7 +26,7 @@ The recommended set is an **_opinionated_** set of rules that we think you shoul 1. They help you adhere to TypeScript best practices. 2. They help catch probable issue vectors in your code. -That being said, it is not the only way to use `@typescript-eslint/eslint-plugin`, nor is it the way that will necesasrily work 100% for your project/company. It has been built based off of two main things: +That being said, it is not the only way to use `@typescript-eslint/eslint-plugin`, nor is it the way that will necessarily work 100% for your project/company. It has been built based off of two main things: 1. TypeScript best practices collected and collated from places like: - [TypeScript repo](https://github.com/Microsoft/TypeScript). diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 53d1cf8a87f6..c3ee46bc95a3 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -31,6 +31,7 @@ "@typescript-eslint/no-empty-function": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-extra-non-null-assertion": "error", "no-extra-parens": "off", "@typescript-eslint/no-extra-parens": "error", "@typescript-eslint/no-extraneous-class": "error", @@ -56,6 +57,7 @@ "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars-experimental": "error", "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "error", "no-useless-constructor": "off", @@ -65,6 +67,8 @@ "@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/prefer-includes": "error", "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", "@typescript-eslint/prefer-readonly": "error", "@typescript-eslint/prefer-regexp-exec": "error", "@typescript-eslint/prefer-string-starts-ends-with": "error", @@ -76,6 +80,7 @@ "@typescript-eslint/require-await": "error", "@typescript-eslint/restrict-plus-operands": "error", "@typescript-eslint/restrict-template-expressions": "error", + "@typescript-eslint/return-await": "error", "semi": "off", "@typescript-eslint/semi": "error", "space-before-function-paren": "off", diff --git a/packages/eslint-plugin/src/rules/camelcase.ts b/packages/eslint-plugin/src/rules/camelcase.ts index 0928484b629d..a2be8a1c6958 100644 --- a/packages/eslint-plugin/src/rules/camelcase.ts +++ b/packages/eslint-plugin/src/rules/camelcase.ts @@ -8,6 +8,19 @@ import * as util from '../util'; type Options = util.InferOptionsTypeFromRule; type MessageIds = util.InferMessageIdsTypeFromRule; +const schema = util.deepMerge( + Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema, + { + properties: { + genericType: { + enum: ['always', 'never'], + }, + }, + }, +); + export default util.createRule({ name: 'camelcase', meta: { @@ -17,7 +30,7 @@ export default util.createRule({ category: 'Stylistic Issues', recommended: 'error', }, - schema: baseRule.meta.schema, + schema: [schema], messages: baseRule.meta.messages, }, defaultOptions: [ @@ -25,6 +38,7 @@ export default util.createRule({ allow: ['^UNSAFE_'], ignoreDestructuring: false, properties: 'never', + genericType: 'never', }, ], create(context, [options]) { @@ -36,6 +50,7 @@ export default util.createRule({ AST_NODE_TYPES.TSAbstractClassProperty, ]; + const genericType = options.genericType; const properties = options.properties; const allow = (options.allow || []).map(entry => ({ name: entry, @@ -117,6 +132,14 @@ export default util.createRule({ return; } + if (parent && parent.type === AST_NODE_TYPES.TSTypeParameter) { + if (genericType === 'always' && isUnderscored(name)) { + report(node); + } + + return; + } + if (parent && parent.type === AST_NODE_TYPES.OptionalMemberExpression) { // Report underscored object names if ( diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 8c1b853c1472..2206ca0a087d 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -124,6 +124,7 @@ export default util.createRule({ node.parent && (node.parent.type === AST_NODE_TYPES.NewExpression || node.parent.type === AST_NODE_TYPES.CallExpression || + node.parent.type === AST_NODE_TYPES.OptionalCallExpression || node.parent.type === AST_NODE_TYPES.ThrowStatement || node.parent.type === AST_NODE_TYPES.AssignmentPattern) ) { diff --git a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts index d7e5fe18b7b6..6ca5128c7fe8 100644 --- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts +++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts @@ -259,7 +259,8 @@ export default util.createRule({ callee?: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, ): boolean { return ( - parent.type === AST_NODE_TYPES.CallExpression && + (parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.OptionalCallExpression) && // make sure this isn't an IIFE parent.callee !== callee ); diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts index 876761819fd7..3a19936b7123 100644 --- a/packages/eslint-plugin/src/rules/func-call-spacing.ts +++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts @@ -73,8 +73,13 @@ export default util.createRule({ * @private */ function checkSpacing( - node: TSESTree.CallExpression | TSESTree.NewExpression, + node: + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression + | TSESTree.NewExpression, ): void { + const isOptionalCall = util.isOptionalOptionalChain(node); + const closingParenToken = sourceCode.getLastToken(node)!; const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken( node.typeParameters || node.callee, @@ -88,7 +93,10 @@ export default util.createRule({ // new expression with no parens... return; } - const lastCalleeToken = sourceCode.getTokenBefore(openingParenToken)!; + const lastCalleeToken = sourceCode.getTokenBefore( + openingParenToken, + util.isNotOptionalChainPunctuator, + )!; const textBetweenTokens = text .slice(lastCalleeToken.range[1], openingParenToken.range[0]) @@ -108,7 +116,11 @@ export default util.createRule({ * Only autofix if there is no newline * https://github.com/eslint/eslint/issues/7787 */ - if (!hasNewline) { + if ( + !hasNewline && + // don't fix optional calls + !isOptionalCall + ) { return fixer.removeRange([ lastCalleeToken.range[1], openingParenToken.range[0], @@ -119,6 +131,18 @@ export default util.createRule({ }, }); } + } else if (isOptionalCall) { + // disallow: + // foo?. (); + // foo ?.(); + // foo ?. (); + if (hasWhitespace || hasNewline) { + context.report({ + node, + loc: lastCalleeToken.loc.start, + messageId: 'unexpected', + }); + } } else { if (!hasWhitespace) { context.report({ @@ -147,6 +171,7 @@ export default util.createRule({ return { CallExpression: checkSpacing, + OptionalCallExpression: checkSpacing, NewExpression: checkSpacing, }; }, diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index d706378e9340..a4dc193e9394 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -22,6 +22,7 @@ import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; import noExplicitAny from './no-explicit-any'; +import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; @@ -38,10 +39,12 @@ import noThisAlias from './no-this-alias'; import noTypeAlias from './no-type-alias'; import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; +import useDefaultTypeParameter from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; -import noUnusedVars from './no-unused-vars'; import noUntypedPublicSignature from './no-untyped-public-signature'; import noUnusedExpressions from './no-unused-expressions'; +import noUnusedVars from './no-unused-vars'; +import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; @@ -49,6 +52,8 @@ import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; import preferIncludes from './prefer-includes'; import preferNamespaceKeyword from './prefer-namespace-keyword'; +import preferNullishCoalescing from './prefer-nullish-coalescing'; +import preferOptionalChain from './prefer-optional-chain'; import preferReadonly from './prefer-readonly'; import preferRegexpExec from './prefer-regexp-exec'; import preferStringStartsEndsWith from './prefer-string-starts-ends-with'; @@ -58,6 +63,7 @@ import requireArraySortCompare from './require-array-sort-compare'; import requireAwait from './require-await'; import restrictPlusOperands from './restrict-plus-operands'; import restrictTemplateExpressions from './restrict-template-expressions'; +import returnAwait from './return-await'; import semi from './semi'; import spaceBeforeFunctionParen from './space-before-function-paren'; import strictBooleanExpressions from './strict-boolean-expressions'; @@ -66,7 +72,6 @@ import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; -import useDefaultTypeParameter from './no-unnecessary-type-arguments'; export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, @@ -93,6 +98,7 @@ export default { 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, 'no-explicit-any': noExplicitAny, + 'no-extra-non-null-assertion': noExtraNonNullAssertion, 'no-extra-parens': noExtraParens, 'no-extraneous-class': noExtraneousClass, 'no-floating-promises': noFloatingPromises, @@ -113,6 +119,7 @@ export default { 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-untyped-public-signature': noUntypedPublicSignature, 'no-unused-vars': noUnusedVars, + 'no-unused-vars-experimental': noUnusedVarsExperimental, 'no-unused-expressions': noUnusedExpressions, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, @@ -121,6 +128,8 @@ export default { 'prefer-function-type': preferFunctionType, 'prefer-includes': preferIncludes, 'prefer-namespace-keyword': preferNamespaceKeyword, + 'prefer-nullish-coalescing': preferNullishCoalescing, + 'prefer-optional-chain': preferOptionalChain, 'prefer-readonly': preferReadonly, 'prefer-regexp-exec': preferRegexpExec, 'prefer-string-starts-ends-with': preferStringStartsEndsWith, @@ -130,6 +139,7 @@ export default { 'require-await': requireAwait, 'restrict-plus-operands': restrictPlusOperands, 'restrict-template-expressions': restrictTemplateExpressions, + 'return-await': returnAwait, semi: semi, 'space-before-function-paren': spaceBeforeFunctionParen, 'strict-boolean-expressions': strictBooleanExpressions, diff --git a/packages/eslint-plugin/src/rules/no-array-constructor.ts b/packages/eslint-plugin/src/rules/no-array-constructor.ts index 5de11364ab81..749ce2ba7ada 100644 --- a/packages/eslint-plugin/src/rules/no-array-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-array-constructor.ts @@ -26,13 +26,17 @@ export default util.createRule({ * @param node node to evaluate */ function check( - node: TSESTree.CallExpression | TSESTree.NewExpression, + node: + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression + | TSESTree.NewExpression, ): void { if ( node.arguments.length !== 1 && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'Array' && - !node.typeParameters + !node.typeParameters && + !util.isOptionalOptionalChain(node) ) { context.report({ node, @@ -55,6 +59,7 @@ export default util.createRule({ return { CallExpression: check, + OptionalCallExpression: check, NewExpression: check, }; }, diff --git a/packages/eslint-plugin/src/rules/no-dynamic-delete.ts b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts index 0ee1636ea643..6fbdba1058fc 100644 --- a/packages/eslint-plugin/src/rules/no-dynamic-delete.ts +++ b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts @@ -33,14 +33,10 @@ export default util.createRule({ ) { return createPropertyReplacement( member.property, - member.property.value, + `.${member.property.value}`, ); } - if (member.property.type === AST_NODE_TYPES.Identifier) { - return createPropertyReplacement(member.property, member.property.name); - } - return undefined; } @@ -69,7 +65,7 @@ export default util.createRule({ replacement: string, ) { return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => - fixer.replaceTextRange(getTokenRange(property), `.${replacement}`); + fixer.replaceTextRange(getTokenRange(property), replacement); } function getTokenRange(property: TSESTree.Expression): [number, number] { diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts index 55fbdb5337e3..cb43721cac45 100644 --- a/packages/eslint-plugin/src/rules/no-empty-interface.ts +++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts @@ -16,6 +16,7 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', }, + fixable: 'code', messages: { noEmpty: 'An empty interface is equivalent to `{}`.', noEmptyWithSuper: @@ -41,6 +42,8 @@ export default util.createRule({ create(context, [{ allowSingleExtends }]) { return { TSInterfaceDeclaration(node): void { + const sourceCode = context.getSourceCode(); + if (node.body.body.length !== 0) { // interface contains members --> Nothing to report return; @@ -59,6 +62,20 @@ export default util.createRule({ context.report({ node: node.id, messageId: 'noEmptyWithSuper', + fix(fixer) { + if (node.extends && node.extends.length) { + return [ + fixer.replaceText( + node, + `type ${sourceCode.getText( + node.id, + )} = ${sourceCode.getText(node.extends[0])}`, + ), + ]; + } + + return null; + }, }); } } diff --git a/packages/eslint-plugin/src/rules/no-explicit-any.ts b/packages/eslint-plugin/src/rules/no-explicit-any.ts index 249265a7683c..29fb16651a2c 100644 --- a/packages/eslint-plugin/src/rules/no-explicit-any.ts +++ b/packages/eslint-plugin/src/rules/no-explicit-any.ts @@ -11,7 +11,7 @@ export type Options = [ ignoreRestArgs?: boolean; }, ]; -export type MessageIds = 'unexpectedAny'; +export type MessageIds = 'unexpectedAny' | 'suggestUnknown' | 'suggestNever'; export default util.createRule({ name: 'no-explicit-any', @@ -25,6 +25,10 @@ export default util.createRule({ fixable: 'code', messages: { unexpectedAny: 'Unexpected any. Specify a different type.', + suggestUnknown: + 'Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct.', + suggestNever: + "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", }, schema: [ { @@ -172,17 +176,36 @@ export default util.createRule({ return; } - let fix: TSESLint.ReportFixFunction | null = null; + const fixOrSuggest: { + fix: TSESLint.ReportFixFunction | null; + suggest: TSESLint.ReportSuggestionArray | null; + } = { + fix: null, + suggest: [ + { + messageId: 'suggestUnknown', + fix(fixer): TSESLint.RuleFix { + return fixer.replaceText(node, 'unknown'); + }, + }, + { + messageId: 'suggestNever', + fix(fixer): TSESLint.RuleFix { + return fixer.replaceText(node, 'never'); + }, + }, + ], + }; if (fixToUnknown) { - fix = (fixer => + fixOrSuggest.fix = (fixer => fixer.replaceText(node, 'unknown')) as TSESLint.ReportFixFunction; } context.report({ node, messageId: 'unexpectedAny', - fix, + ...fixOrSuggest, }); }, }; diff --git a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts new file mode 100644 index 000000000000..cd116a3a4d7b --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts @@ -0,0 +1,25 @@ +import * as util from '../util'; + +export default util.createRule({ + name: 'no-extra-non-null-assertion', + meta: { + type: 'problem', + docs: { + description: 'Disallow extra non-null assertion', + category: 'Stylistic Issues', + recommended: false, + }, + schema: [], + messages: { + noExtraNonNullAssertion: 'Forbidden extra non-null assertion.', + }, + }, + defaultOptions: [], + create(context) { + return { + 'TSNonNullExpression > TSNonNullExpression'(node): void { + context.report({ messageId: 'noExtraNonNullAssertion', node }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/no-extraneous-class.ts b/packages/eslint-plugin/src/rules/no-extraneous-class.ts index 4756b5928f33..996dd0ea71cd 100644 --- a/packages/eslint-plugin/src/rules/no-extraneous-class.ts +++ b/packages/eslint-plugin/src/rules/no-extraneous-class.ts @@ -9,6 +9,7 @@ type Options = [ allowConstructorOnly?: boolean; allowEmpty?: boolean; allowStaticOnly?: boolean; + allowWithDecorator?: boolean; }, ]; type MessageIds = 'empty' | 'onlyStatic' | 'onlyConstructor'; @@ -36,6 +37,9 @@ export default util.createRule({ allowStaticOnly: { type: 'boolean', }, + allowWithDecorator: { + type: 'boolean', + }, }, }, ], @@ -50,9 +54,24 @@ export default util.createRule({ allowConstructorOnly: false, allowEmpty: false, allowStaticOnly: false, + allowWithDecorator: false, }, ], - create(context, [{ allowConstructorOnly, allowEmpty, allowStaticOnly }]) { + create( + context, + [{ allowConstructorOnly, allowEmpty, allowStaticOnly, allowWithDecorator }], + ) { + const isAllowWithDecorator = ( + node: TSESTree.ClassDeclaration | TSESTree.ClassExpression | undefined, + ): boolean => { + return !!( + allowWithDecorator && + node && + node.decorators && + node.decorators.length + ); + }; + return { ClassBody(node): void { const parent = node.parent as @@ -65,9 +84,8 @@ export default util.createRule({ } const reportNode = 'id' in parent && parent.id ? parent.id : parent; - if (node.body.length === 0) { - if (allowEmpty) { + if (allowEmpty || isAllowWithDecorator(parent)) { return; } diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts index 92a582526795..f1219e67dc4b 100644 --- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts +++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts @@ -54,7 +54,8 @@ export default util.createRule({ callName: string, ): boolean { return ( - init.type === AST_NODE_TYPES.CallExpression && + (init.type === AST_NODE_TYPES.CallExpression || + init.type === AST_NODE_TYPES.OptionalCallExpression) && init.callee.type === AST_NODE_TYPES.Identifier && init.callee.name === callName ); diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 63d0f0bf483c..50d6cf9201a5 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -71,6 +71,7 @@ export default util.createRule({ const voidReturnChecks: TSESLint.RuleListener = { CallExpression: checkArguments, + OptionalCallExpression: checkArguments, NewExpression: checkArguments, }; @@ -93,7 +94,10 @@ export default util.createRule({ } function checkArguments( - node: TSESTree.CallExpression | TSESTree.NewExpression, + node: + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression + | TSESTree.NewExpression, ): void { const tsNode = parserServices.esTreeNodeToTSNodeMap.get< ts.CallExpression | ts.NewExpression diff --git a/packages/eslint-plugin/src/rules/no-require-imports.ts b/packages/eslint-plugin/src/rules/no-require-imports.ts index 91d81e0d8c53..7b382e6bbaf0 100644 --- a/packages/eslint-plugin/src/rules/no-require-imports.ts +++ b/packages/eslint-plugin/src/rules/no-require-imports.ts @@ -18,7 +18,7 @@ export default util.createRule({ defaultOptions: [], create(context) { return { - 'CallExpression > Identifier[name="require"]'( + 'CallExpression > Identifier[name="require"], OptionalCallExpression > Identifier[name="require"]'( node: TSESTree.Identifier, ): void { context.report({ diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 8e75f7adb3fd..420271a23b86 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -41,15 +41,9 @@ const isLiteral = (type: ts.Type): boolean => isLiteralType(type); // #endregion -type ExpressionWithTest = - | TSESTree.ConditionalExpression - | TSESTree.DoWhileStatement - | TSESTree.ForStatement - | TSESTree.IfStatement - | TSESTree.WhileStatement; - export type Options = [ { + allowConstantLoopConditions?: boolean; ignoreRhs?: boolean; }, ]; @@ -74,6 +68,9 @@ export default createRule({ { type: 'object', properties: { + allowConstantLoopConditions: { + type: 'boolean', + }, ignoreRhs: { type: 'boolean', }, @@ -91,10 +88,11 @@ export default createRule({ }, defaultOptions: [ { + allowConstantLoopConditions: false, ignoreRhs: false, }, ], - create(context, [{ ignoreRhs }]) { + create(context, [{ allowConstantLoopConditions, ignoreRhs }]) { const service = getParserServices(context); const checker = service.program.getTypeChecker(); @@ -165,14 +163,13 @@ export default createRule({ * Filters all LogicalExpressions to prevent some duplicate reports. */ function checkIfTestExpressionIsNecessaryConditional( - node: ExpressionWithTest, + node: TSESTree.ConditionalExpression | TSESTree.IfStatement, ): void { - if ( - node.test !== null && - node.test.type !== AST_NODE_TYPES.LogicalExpression - ) { - checkNode(node.test); + if (node.test.type === AST_NODE_TYPES.LogicalExpression) { + return; } + + checkNode(node.test); } /** @@ -187,14 +184,46 @@ export default createRule({ } } + /** + * Checks that a testable expression of a loop is necessarily conditional, reports otherwise. + */ + function checkIfLoopIsNecessaryConditional( + node: + | TSESTree.DoWhileStatement + | TSESTree.ForStatement + | TSESTree.WhileStatement, + ): void { + if ( + node.test === null || + node.test.type === AST_NODE_TYPES.LogicalExpression + ) { + return; + } + + /** + * Allow: + * while (true) {} + * for (;true;) {} + * do {} while (true) + */ + if ( + allowConstantLoopConditions && + isBooleanLiteralType(getNodeType(node.test), true) + ) { + return; + } + + checkNode(node.test); + } + return { BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional, ConditionalExpression: checkIfTestExpressionIsNecessaryConditional, - DoWhileStatement: checkIfTestExpressionIsNecessaryConditional, - ForStatement: checkIfTestExpressionIsNecessaryConditional, + DoWhileStatement: checkIfLoopIsNecessaryConditional, + ForStatement: checkIfLoopIsNecessaryConditional, IfStatement: checkIfTestExpressionIsNecessaryConditional, - WhileStatement: checkIfTestExpressionIsNecessaryConditional, LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals, + WhileStatement: checkIfLoopIsNecessaryConditional, }; }, }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 141959ca7afd..29f98bdedf00 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -245,9 +245,7 @@ export default util.createRule({ node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, ): void { if ( - options && - options.typesToIgnore && - options.typesToIgnore.includes( + options?.typesToIgnore?.includes( sourceCode.getText(node.typeAnnotation), ) ) { diff --git a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts index 2ecb1cbd62c6..39a635b850e7 100644 --- a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts +++ b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts @@ -106,7 +106,10 @@ export default util.createRule({ }); } - if (!isReturnTyped(node.value.returnType)) { + if ( + node.kind !== 'constructor' && + !isReturnTyped(node.value.returnType) + ) { context.report({ node, messageId: 'noReturnType', diff --git a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts new file mode 100644 index 000000000000..1286982895ef --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts @@ -0,0 +1,364 @@ +/* eslint-disable no-fallthrough */ + +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; +import * as util from '../util'; + +export type Options = [ + { + ignoredNamesRegex?: string | boolean; + ignoreArgsIfArgsAfterAreUsed?: boolean; + }, +]; +export type MessageIds = + | 'unused' + | 'unusedWithIgnorePattern' + | 'unusedImport' + | 'unusedTypeParameters'; + +type NodeWithTypeParams = ts.Node & { + typeParameters: ts.NodeArray; +}; + +export const DEFAULT_IGNORED_REGEX_STRING = '^_'; +export default util.createRule({ + name: 'no-unused-vars-experimental', + meta: { + type: 'problem', + docs: { + description: 'Disallow unused variables and arguments.', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + schema: [ + { + type: 'object', + properties: { + ignoredNamesRegex: { + oneOf: [ + { + type: 'string', + }, + { + type: 'boolean', + enum: [false], + }, + ], + }, + ignoreArgsIfArgsAfterAreUsed: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: { + unused: "{{type}} '{{name}}' is declared but its value is never read.", + unusedWithIgnorePattern: + "{{type}} '{{name}}' is declared but its value is never read. Allowed unused names must match {{pattern}}", + unusedImport: 'All imports in import declaration are unused.', + unusedTypeParameters: 'All type parameters are unused.', + }, + }, + defaultOptions: [ + { + ignoredNamesRegex: DEFAULT_IGNORED_REGEX_STRING, + ignoreArgsIfArgsAfterAreUsed: false, + }, + ], + create(context, [userOptions]) { + const parserServices = util.getParserServices(context); + const tsProgram = parserServices.program; + const afterAllDiagnosticsCallbacks: (() => void)[] = []; + + const options = { + ignoredNames: + userOptions && typeof userOptions.ignoredNamesRegex === 'string' + ? new RegExp(userOptions.ignoredNamesRegex) + : null, + ignoreArgsIfArgsAfterAreUsed: + userOptions.ignoreArgsIfArgsAfterAreUsed || false, + }; + + function handleIdentifier(identifier: ts.Identifier): void { + function report(type: string): void { + const node = parserServices.tsNodeToESTreeNodeMap.get(identifier); + const regex = options.ignoredNames; + const name = identifier.getText(); + if (regex) { + if (!regex.test(name)) { + context.report({ + node, + messageId: 'unusedWithIgnorePattern', + data: { + name, + type, + pattern: regex.toString(), + }, + }); + } + } else { + context.report({ + node, + messageId: 'unused', + data: { + name, + type, + }, + }); + } + } + + const parent = identifier.parent; + + // is a single variable diagnostic + switch (parent.kind) { + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.ObjectBindingPattern: + report('Destructured Variable'); + break; + + case ts.SyntaxKind.ClassDeclaration: + report('Class'); + break; + + case ts.SyntaxKind.EnumDeclaration: + report('Enum'); + break; + + case ts.SyntaxKind.FunctionDeclaration: + report('Function'); + break; + + // this won't happen because there are specific nodes that wrap up named/default import identifiers + // case ts.SyntaxKind.ImportDeclaration: + // import equals is always treated as a variable + case ts.SyntaxKind.ImportEqualsDeclaration: + // the default import is NOT used, but a named import is used + case ts.SyntaxKind.ImportClause: + // a named import is NOT used, but either another named import, or the default import is used + case ts.SyntaxKind.ImportSpecifier: + // a namespace import is NOT used, but the default import is used + case ts.SyntaxKind.NamespaceImport: + report('Import'); + break; + + case ts.SyntaxKind.InterfaceDeclaration: + report('Interface'); + break; + + case ts.SyntaxKind.MethodDeclaration: + report('Method'); + break; + + case ts.SyntaxKind.Parameter: + handleParameterDeclaration( + identifier, + parent as ts.ParameterDeclaration, + ); + break; + + case ts.SyntaxKind.PropertyDeclaration: + report('Property'); + break; + + case ts.SyntaxKind.TypeAliasDeclaration: + report('Type'); + break; + + case ts.SyntaxKind.TypeParameter: + handleTypeParam(identifier); + break; + + case ts.SyntaxKind.VariableDeclaration: + report('Variable'); + break; + + default: + throw new Error(`Unknown node with kind ${parent.kind}.`); + // TODO - should we just handle this gracefully? + // report('Unknown Node'); + // break; + } + } + + const unusedParameters = new Set(); + function handleParameterDeclaration( + identifier: ts.Identifier, + parent: ts.ParameterDeclaration, + ): void { + const name = identifier.getText(); + // regardless of if the paramter is ignored, track that it had a diagnostic fired on it + unusedParameters.add(identifier); + + /* + NOTE - Typescript will automatically ignore parameters that have a + leading underscore in their name. We cannot do anything about this. + */ + + function report(): void { + const node = parserServices.tsNodeToESTreeNodeMap.get(identifier); + context.report({ + node, + messageId: 'unused', + data: { + name, + type: 'Parameter', + }, + }); + } + + const isLastParameter = + parent.parent.parameters.indexOf(parent) === + parent.parent.parameters.length - 1; + if (!isLastParameter && options.ignoreArgsIfArgsAfterAreUsed) { + // once all diagnostics are processed, we can check if the following args are unused + afterAllDiagnosticsCallbacks.push(() => { + for (const param of parent.parent.parameters) { + if (!unusedParameters.has(param.name)) { + return; + } + } + + // none of the following params were unused, so report + report(); + }); + } else { + report(); + } + } + + function handleImportDeclaration(parent: ts.ImportDeclaration): void { + // the entire import statement is unused + + /* + NOTE - Typescript will automatically ignore imports that have a + leading underscore in their name. We cannot do anything about this. + */ + + context.report({ + messageId: 'unusedImport', + node: parserServices.tsNodeToESTreeNodeMap.get(parent), + }); + } + + function handleDestructure(parent: ts.BindingPattern): void { + // the entire desctructure is unused + // note that this case only ever triggers for simple, single-level destructured objects + // i.e. these will not trigger it: + // - const {a:_a, b, c: {d}} = z; + // - const [a, b] = c; + + parent.elements.forEach(element => { + if (element.kind === ts.SyntaxKind.BindingElement) { + const name = element.name; + if (name.kind === ts.SyntaxKind.Identifier) { + handleIdentifier(name); + } + } + }); + } + + function handleTypeParamList(node: NodeWithTypeParams): void { + // the entire generic decl list is unused + + /* + NOTE - Typescript will automatically ignore generics that have a + leading underscore in their name. We cannot do anything about this. + */ + + const parent = parserServices.tsNodeToESTreeNodeMap.get( + node as never, + ) as { + typeParameters: TSESTree.TSTypeParameterDeclaration; + }; + context.report({ + messageId: 'unusedTypeParameters', + node: parent.typeParameters, + }); + } + function handleTypeParam(identifier: ts.Identifier): void { + context.report({ + node: parserServices.tsNodeToESTreeNodeMap.get(identifier), + messageId: 'unused', + data: { + name: identifier.getText(), + type: 'Type Parameter', + }, + }); + } + + return { + 'Program:exit'(program: TSESTree.Node): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(program); + const sourceFile = util.getSourceFileOfNode(tsNode); + const diagnostics = tsProgram.getSemanticDiagnostics(sourceFile); + + diagnostics.forEach(diag => { + if (isUnusedDiagnostic(diag.code)) { + if (diag.start !== undefined) { + const node = util.getTokenAtPosition(sourceFile, diag.start); + const parent = node.parent; + if (isIdentifier(node)) { + handleIdentifier(node); + } else if (isImport(parent)) { + handleImportDeclaration(parent); + } else if (isDestructure(parent)) { + handleDestructure(parent); + } else if (isGeneric(node, parent)) { + handleTypeParamList(parent); + } + } + } + }); + + // trigger all the checks to be done after all the diagnostics have been evaluated + afterAllDiagnosticsCallbacks.forEach(cb => cb()); + }, + }; + }, +}); + +/** + * Checks if the diagnostic code is one of the expected "unused var" codes + */ +function isUnusedDiagnostic(code: number): boolean { + return [ + 6133, // '{0}' is declared but never used. + 6138, // Property '{0}' is declared but its value is never read. + 6192, // All imports in import declaration are unused. + 6196, // '{0}' is declared but its value is never read. + 6198, // All destructured elements are unused. + 6199, // All variables are unused. + 6205, // All type parameters are unused. + ].includes(code); +} + +/** + * Checks if the given node is a destructuring pattern + */ +function isDestructure(node: ts.Node): node is ts.BindingPattern { + return ( + node.kind === ts.SyntaxKind.ObjectBindingPattern || + node.kind === ts.SyntaxKind.ArrayBindingPattern + ); +} + +function isImport(node: ts.Node): node is ts.ImportDeclaration { + return node.kind === ts.SyntaxKind.ImportDeclaration; +} + +function isIdentifier(node: ts.Node): node is ts.Identifier { + return node.kind === ts.SyntaxKind.Identifier; +} + +function isGeneric( + node: ts.Node, + parent: ts.Node & Partial, +): parent is NodeWithTypeParams { + return ( + node.kind === ts.SyntaxKind.LessThanToken && + parent.typeParameters !== undefined + ); +} diff --git a/packages/eslint-plugin/src/rules/no-var-requires.ts b/packages/eslint-plugin/src/rules/no-var-requires.ts index 274cf2c1ceb2..66ac16a4e220 100644 --- a/packages/eslint-plugin/src/rules/no-var-requires.ts +++ b/packages/eslint-plugin/src/rules/no-var-requires.ts @@ -1,4 +1,7 @@ -import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import * as util from '../util'; type Options = []; @@ -22,13 +25,16 @@ export default util.createRule({ defaultOptions: [], create(context) { return { - CallExpression(node): void { + 'CallExpression, OptionalCallExpression'( + node: TSESTree.CallExpression | TSESTree.OptionalCallExpression, + ): void { if ( node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'require' && node.parent && (node.parent.type === AST_NODE_TYPES.VariableDeclarator || - node.parent.type === AST_NODE_TYPES.CallExpression) + node.parent.type === AST_NODE_TYPES.CallExpression || + node.parent.type === AST_NODE_TYPES.OptionalCallExpression) ) { context.report({ node, diff --git a/packages/eslint-plugin/src/rules/prefer-for-of.ts b/packages/eslint-plugin/src/rules/prefer-for-of.ts index 5aef0845a533..80180415ace3 100644 --- a/packages/eslint-plugin/src/rules/prefer-for-of.ts +++ b/packages/eslint-plugin/src/rules/prefer-for-of.ts @@ -58,7 +58,8 @@ export default util.createRule({ node.type === AST_NODE_TYPES.BinaryExpression && node.operator === '<' && isMatchingIdentifier(node.left, name) && - node.right.type === AST_NODE_TYPES.MemberExpression && + (node.right.type === AST_NODE_TYPES.MemberExpression || + node.right.type === AST_NODE_TYPES.OptionalMemberExpression) && isMatchingIdentifier(node.right.property, 'length') ) { return node.right.object; @@ -172,7 +173,8 @@ export default util.createRule({ return ( !contains(body, id) || (node !== undefined && - node.type === AST_NODE_TYPES.MemberExpression && + (node.type === AST_NODE_TYPES.MemberExpression || + node.type === AST_NODE_TYPES.OptionalMemberExpression) && node.property === id && sourceCode.getText(node.object) === arrayText && !isAssignee(node)) diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index fcb5dfca4f57..7ae89cb8f67c 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -1,4 +1,7 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; import { getStaticValue } from 'eslint-utils'; import { AST as RegExpAST, parseRegExpLiteral } from 'regexpp'; import ts from 'typescript'; @@ -119,11 +122,18 @@ export default createRule({ } return { - "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]"( - node: TSESTree.MemberExpression, + [[ + "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]", + "BinaryExpression > OptionalCallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]", + "BinaryExpression > CallExpression.left > OptionalMemberExpression.callee[property.name='indexOf'][computed=false]", + "BinaryExpression > OptionalCallExpression.left > OptionalMemberExpression.callee[property.name='indexOf'][computed=false]", + ].join(', ')]( + node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression, ): void { // Check if the comparison is equivalent to `includes()`. - const callNode = node.parent as TSESTree.CallExpression; + const callNode = node.parent as + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression; const compareNode = callNode.parent as TSESTree.BinaryExpression; const negative = isNegativeCheck(compareNode); if (!negative && !isPositiveCheck(compareNode)) { @@ -171,10 +181,17 @@ export default createRule({ }, // /bar/.test(foo) - 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'( - node: TSESTree.MemberExpression, + [[ + 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]', + 'OptionalCallExpression > MemberExpression.callee[property.name="test"][computed=false]', + 'CallExpression > OptionalMemberExpression.callee[property.name="test"][computed=false]', + 'OptionalCallExpression > OptionalMemberExpression.callee[property.name="test"][computed=false]', + ].join(', ')]( + node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression, ): void { - const callNode = node.parent as TSESTree.CallExpression; + const callNode = node.parent as + | TSESTree.CallExpression + | TSESTree.OptionalCallExpression; const text = callNode.arguments.length === 1 ? parseRegExp(node.object) : null; if (text == null) { @@ -191,7 +208,9 @@ export default createRule({ argNode.type !== 'TemplateLiteral' && argNode.type !== 'Identifier' && argNode.type !== 'MemberExpression' && - argNode.type !== 'CallExpression'; + argNode.type !== 'OptionalMemberExpression' && + argNode.type !== 'CallExpression' && + argNode.type !== 'OptionalCallExpression'; yield fixer.removeRange([callNode.range[0], argNode.range[0]]); if (needsParen) { @@ -200,7 +219,11 @@ export default createRule({ } yield fixer.insertTextAfter( argNode, - `.includes(${JSON.stringify(text)}`, + `${ + callNode.type === AST_NODE_TYPES.OptionalCallExpression + ? '?.' + : '.' + }includes(${JSON.stringify(text)}`, ); }, }); diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts new file mode 100644 index 000000000000..afb564025fd7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -0,0 +1,174 @@ +import { + AST_NODE_TYPES, + AST_TOKEN_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; +import * as util from '../util'; + +export type Options = [ + { + ignoreConditionalTests?: boolean; + ignoreMixedLogicalExpressions?: boolean; + }, +]; +export type MessageIds = 'preferNullish'; + +export default util.createRule({ + name: 'prefer-nullish-coalescing', + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce the usage of the nullish coalescing operator instead of logical chaining', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferNullish: + 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', + }, + schema: [ + { + type: 'object', + properties: { + ignoreConditionalTests: { + type: 'boolean', + }, + ignoreMixedLogicalExpressions: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + ignoreConditionalTests: true, + ignoreMixedLogicalExpressions: true, + }, + ], + create(context, [{ ignoreConditionalTests, ignoreMixedLogicalExpressions }]) { + const parserServices = util.getParserServices(context); + const sourceCode = context.getSourceCode(); + const checker = parserServices.program.getTypeChecker(); + + return { + 'LogicalExpression[operator = "||"]'( + node: TSESTree.LogicalExpression, + ): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get< + ts.BinaryExpression + >(node); + const type = checker.getTypeAtLocation(tsNode.left); + const isNullish = util.isNullableType(type, { allowUndefined: true }); + if (!isNullish) { + return; + } + + if (ignoreConditionalTests === true && isConditionalTest(node)) { + return; + } + + const isMixedLogical = isMixedLogicalExpression(node); + if (ignoreMixedLogicalExpressions === true && isMixedLogical) { + return; + } + + const barBarOperator = sourceCode.getTokenAfter( + node.left, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === node.operator, + )!; // there _must_ be an operator + + const fixer = isMixedLogical + ? // suggestion instead for cases where we aren't sure if the fixer is completely safe + ({ + suggest: [ + { + messageId: 'preferNullish', + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText(barBarOperator, '??'); + }, + }, + ], + } as const) + : { + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText(barBarOperator, '??'); + }, + }; + + context.report({ + node: barBarOperator, + messageId: 'preferNullish', + ...fixer, + }); + }, + }; + }, +}); + +function isConditionalTest(node: TSESTree.Node): boolean { + const parents = new Set([node]); + let current = node.parent; + while (current) { + parents.add(current); + + if ( + (current.type === AST_NODE_TYPES.ConditionalExpression || + current.type === AST_NODE_TYPES.DoWhileStatement || + current.type === AST_NODE_TYPES.IfStatement || + current.type === AST_NODE_TYPES.ForStatement || + current.type === AST_NODE_TYPES.WhileStatement) && + parents.has(current.test) + ) { + return true; + } + + if ( + [ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionExpression, + ].includes(current.type) + ) { + /** + * This is a weird situation like: + * `if (() => a || b) {}` + * `if (function () { return a || b }) {}` + */ + return false; + } + + current = current.parent; + } + + return false; +} + +function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean { + const seen = new Set(); + const queue = [node.parent, node.left, node.right]; + for (const current of queue) { + if (seen.has(current)) { + continue; + } + seen.add(current); + + if (current && current.type === AST_NODE_TYPES.LogicalExpression) { + if (current.operator === '&&') { + return true; + } else if (current.operator === '||') { + // check the pieces of the node to catch cases like `a || b || c && d` + queue.push(current.parent, current.left, current.right); + } + } + } + + return false; +} diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts new file mode 100644 index 000000000000..a7bcc49a678b --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -0,0 +1,198 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +const WHITESPACE_REGEX = /\s/g; + +/* +The AST is always constructed such the first element is always the deepest element. + +I.e. for this code: `foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz` +The AST will look like this: +{ + left: { + left: { + left: foo + right: foo.bar + } + right: foo.bar.baz + } + right: foo.bar.baz.buzz +} +*/ +export default util.createRule({ + name: 'prefer-optional-chain', + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer using concise optional chain expressions instead of chained logical ands', + category: 'Best Practices', + recommended: false, + }, + fixable: 'code', + messages: { + preferOptionalChain: + "Prefer using an optional chain expression instead, as it's more concise and easier to read.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + return { + [[ + 'LogicalExpression[operator="&&"] > Identifier', + 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]', + 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]', + ].join(',')]( + initialIdentifierOrNotEqualsExpr: + | TSESTree.BinaryExpression + | TSESTree.Identifier, + ): void { + // selector guarantees this cast + const initialExpression = initialIdentifierOrNotEqualsExpr.parent as TSESTree.LogicalExpression; + + if (initialExpression.left !== initialIdentifierOrNotEqualsExpr) { + // the identifier is not the deepest left node + return; + } + if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { + return; + } + + // walk up the tree to figure out how many logical expressions we can include + let previous: TSESTree.LogicalExpression = initialExpression; + let current: TSESTree.Node = initialExpression; + let previousLeftText = getText(initialIdentifierOrNotEqualsExpr); + let optionallyChainedCode = previousLeftText; + let expressionCount = 1; + while (current.type === AST_NODE_TYPES.LogicalExpression) { + if (!isValidChainTarget(current.right)) { + break; + } + + const leftText = previousLeftText; + const rightText = getText(current.right); + if (!rightText.startsWith(leftText)) { + break; + } + expressionCount += 1; + + // omit weird doubled up expression that make no sense like foo.bar && foo.bar + if (rightText !== leftText) { + previousLeftText = rightText; + + /* + Diff the left and right text to construct the fix string + There are the following cases: + + 1) + rightText === 'foo.bar.baz.buzz' + leftText === 'foo.bar.baz' + diff === '.buzz' + + 2) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz' + diff === '.buzz()' + + 3) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz.buzz' + diff === '()' + + 4) + rightText === 'foo.bar.baz[buzz]' + leftText === 'foo.bar.baz' + diff === '[buzz]' + */ + const diff = rightText.replace(leftText, ''); + const needsDot = diff.startsWith('(') || diff.startsWith('['); + optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; + } + + /* istanbul ignore if: this shouldn't ever happen, but types */ + if (!current.parent) { + break; + } + previous = current; + current = current.parent; + } + + if (expressionCount > 1) { + context.report({ + node: previous, + messageId: 'preferOptionalChain', + fix(fixer) { + return fixer.replaceText(previous, optionallyChainedCode); + }, + }); + } + }, + }; + + function getText( + node: + | TSESTree.BinaryExpression + | TSESTree.CallExpression + | TSESTree.Identifier + | TSESTree.MemberExpression, + ): string { + const text = sourceCode.getText( + node.type === AST_NODE_TYPES.BinaryExpression ? node.left : node, + ); + + // Removes spaces from the source code for the given node + return text.replace(WHITESPACE_REGEX, ''); + } + }, +}); + +function isValidChainTarget( + node: TSESTree.Node, + allowIdentifier = false, +): node is + | TSESTree.BinaryExpression + | TSESTree.CallExpression + | TSESTree.MemberExpression { + if ( + node.type === AST_NODE_TYPES.MemberExpression || + node.type === AST_NODE_TYPES.CallExpression + ) { + return true; + } + if (allowIdentifier && node.type === AST_NODE_TYPES.Identifier) { + return true; + } + + /* + special case for the following, where we only want the left + - foo !== null + - foo != null + - foo !== undefined + - foo != undefined + */ + if ( + node.type === AST_NODE_TYPES.BinaryExpression && + ['!==', '!='].includes(node.operator) && + isValidChainTarget(node.left, allowIdentifier) + ) { + if ( + node.right.type === AST_NODE_TYPES.Identifier && + node.right.name === 'undefined' + ) { + return true; + } + if ( + node.right.type === AST_NODE_TYPES.Literal && + node.right.value === null + ) { + return true; + } + } + + return false; +} diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index 5f27f8ad8a1d..4db47f9e075b 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -42,13 +42,12 @@ export default util.createRule({ } return { - 'FunctionDeclaration[async = true]': rules.FunctionDeclaration, - 'FunctionExpression[async = true]': rules.FunctionExpression, + FunctionDeclaration: rules.FunctionDeclaration, + FunctionExpression: rules.FunctionExpression, + ArrowFunctionExpression: rules.ArrowFunctionExpression, 'ArrowFunctionExpression[async = true]'( node: TSESTree.ArrowFunctionExpression, ): void { - rules.ArrowFunctionExpression(node); - // If body type is not BlockStatment, we need to check the return type here if (node.body.type !== AST_NODE_TYPES.BlockStatement) { const expression = parserServices.esTreeNodeToTSNodeMap.get( @@ -56,15 +55,13 @@ export default util.createRule({ ); if (expression && isThenableType(expression)) { // tell the base rule to mark the scope as having an await so it ignores it - rules.AwaitExpression(node as never); + rules.AwaitExpression(); } } }, - 'FunctionDeclaration[async = true]:exit': - rules['FunctionDeclaration:exit'], - 'FunctionExpression[async = true]:exit': rules['FunctionExpression:exit'], - 'ArrowFunctionExpression[async = true]:exit': - rules['ArrowFunctionExpression:exit'], + 'FunctionDeclaration:exit': rules['FunctionDeclaration:exit'], + 'FunctionExpression:exit': rules['FunctionExpression:exit'], + 'ArrowFunctionExpression:exit': rules['ArrowFunctionExpression:exit'], AwaitExpression: rules.AwaitExpression, ForOfStatement: rules.ForOfStatement, @@ -74,7 +71,7 @@ export default util.createRule({ >(node); if (expression && isThenableType(expression)) { // tell the base rule to mark the scope as having an await so it ignores it - rules.AwaitExpression(node as never); + rules.AwaitExpression(); } }, }; diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index 1f715f1f9022..c41587a169ac 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -2,7 +2,14 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import ts from 'typescript'; import * as util from '../util'; -export default util.createRule({ +type Options = [ + { + checkCompoundAssignments?: boolean; + }, +]; +type MessageIds = 'notNumbers' | 'notStrings' | 'notBigInts'; + +export default util.createRule({ name: 'restrict-plus-operands', meta: { type: 'problem', @@ -20,12 +27,25 @@ export default util.createRule({ "Operands of '+' operation must either be both strings or both numbers. Consider using a template literal.", notBigInts: "Operands of '+' operation must be both bigints.", }, - schema: [], + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + checkCompoundAssignments: { + type: 'boolean', + }, + }, + }, + ], }, - defaultOptions: [], - create(context) { + defaultOptions: [ + { + checkCompoundAssignments: false, + }, + ], + create(context, [{ checkCompoundAssignments }]) { const service = util.getParserServices(context); - const typeChecker = service.program.getTypeChecker(); type BaseLiteral = 'string' | 'number' | 'bigint' | 'invalid'; @@ -83,32 +103,41 @@ export default util.createRule({ return getBaseTypeOfLiteralType(type); } - return { - "BinaryExpression[operator='+']"(node: TSESTree.BinaryExpression): void { - const leftType = getNodeType(node.left); - const rightType = getNodeType(node.right); + function checkPlusOperands( + node: TSESTree.BinaryExpression | TSESTree.AssignmentExpression, + ): void { + const leftType = getNodeType(node.left); + const rightType = getNodeType(node.right); - if ( - leftType === 'invalid' || - rightType === 'invalid' || - leftType !== rightType - ) { - if (leftType === 'string' || rightType === 'string') { - context.report({ - node, - messageId: 'notStrings', - }); - } else if (leftType === 'bigint' || rightType === 'bigint') { - context.report({ - node, - messageId: 'notBigInts', - }); - } else { - context.report({ - node, - messageId: 'notNumbers', - }); - } + if ( + leftType === 'invalid' || + rightType === 'invalid' || + leftType !== rightType + ) { + if (leftType === 'string' || rightType === 'string') { + context.report({ + node, + messageId: 'notStrings', + }); + } else if (leftType === 'bigint' || rightType === 'bigint') { + context.report({ + node, + messageId: 'notBigInts', + }); + } else { + context.report({ + node, + messageId: 'notNumbers', + }); + } + } + } + + return { + "BinaryExpression[operator='+']": checkPlusOperands, + "AssignmentExpression[operator='+=']"(node): void { + if (checkCompoundAssignments) { + checkPlusOperands(node); } }, }; diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts new file mode 100644 index 000000000000..b96c612fd7a7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -0,0 +1,156 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import * as tsutils from 'tsutils'; +import ts, { SyntaxKind } from 'typescript'; +import * as util from '../util'; + +export default util.createRule({ + name: 'return-await', + meta: { + docs: { + description: 'Rules for awaiting returned promises', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + type: 'problem', + messages: { + nonPromiseAwait: + 'returning an awaited value that is not a promise is not allowed', + disallowedPromiseAwait: + 'returning an awaited promise is not allowed in this context', + requiredPromiseAwait: + 'returning an awaited promise is required in this context', + }, + schema: [ + { + enum: ['in-try-catch', 'always', 'never'], + }, + ], + }, + defaultOptions: ['in-try-catch'], + + create(context, [option]) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + function inTryCatch(node: ts.Node): boolean { + let ancestor = node.parent; + + while (ancestor && !ts.isFunctionLike(ancestor)) { + if ( + tsutils.isTryStatement(ancestor) || + tsutils.isCatchClause(ancestor) + ) { + return true; + } + + ancestor = ancestor.parent; + } + + return false; + } + + function test( + node: TSESTree.ReturnStatement | TSESTree.ArrowFunctionExpression, + expression: ts.Node, + ): void { + let child: ts.Node; + + const isAwait = expression.kind === SyntaxKind.AwaitExpression; + + if (isAwait) { + child = expression.getChildAt(1); + } else { + child = expression; + } + + const type = checker.getTypeAtLocation(child); + + const isThenable = + tsutils.isTypeFlagSet(type, ts.TypeFlags.Any) || + tsutils.isTypeFlagSet(type, ts.TypeFlags.Unknown) || + tsutils.isThenableType(checker, expression, type); + + if (!isAwait && !isThenable) { + return; + } + + if (isAwait && !isThenable) { + context.report({ + messageId: 'nonPromiseAwait', + node, + }); + return; + } + + if (option === 'always') { + if (!isAwait && isThenable) { + context.report({ + messageId: 'requiredPromiseAwait', + node, + }); + } + + return; + } + + if (option === 'never') { + if (isAwait) { + context.report({ + messageId: 'disallowedPromiseAwait', + node, + }); + } + + return; + } + + if (option === 'in-try-catch') { + const isInTryCatch = inTryCatch(expression); + if (isAwait && !isInTryCatch) { + context.report({ + messageId: 'disallowedPromiseAwait', + node, + }); + } else if (!isAwait && isInTryCatch) { + context.report({ + messageId: 'requiredPromiseAwait', + node, + }); + } + + return; + } + } + + return { + 'ArrowFunctionExpression[async = true]:exit'( + node: TSESTree.ArrowFunctionExpression, + ): void { + if (node.body.type !== AST_NODE_TYPES.BlockStatement) { + const expression = parserServices.esTreeNodeToTSNodeMap.get( + node.body, + ); + + test(node, expression); + } + }, + ReturnStatement(node): void { + const originalNode = parserServices.esTreeNodeToTSNodeMap.get< + ts.ReturnStatement + >(node); + + const { expression } = originalNode; + + if (!expression) { + return; + } + + test(node, expression); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index 66f783ec1a87..c704d15b7ef2 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -108,12 +108,8 @@ export default util.createRule({ messageId: 'singleParameterDifference', data: { failureStringStart: failureStringStart(lineOfOtherOverload), - type1: sourceCode.getText( - typeAnnotation0 && typeAnnotation0.typeAnnotation, - ), - type2: sourceCode.getText( - typeAnnotation1 && typeAnnotation1.typeAnnotation, - ), + type1: sourceCode.getText(typeAnnotation0?.typeAnnotation), + type2: sourceCode.getText(typeAnnotation1?.typeAnnotation), }, node: p1, }); diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index 6dae402dce9f..fccbafeede78 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -1 +1,39 @@ -export const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/; +import { + TSESTree, + AST_TOKEN_TYPES, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; + +const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/; + +function isOptionalChainPunctuator( + token: TSESTree.Token | TSESTree.Comment, +): boolean { + return token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.'; +} +function isNotOptionalChainPunctuator( + token: TSESTree.Token | TSESTree.Comment, +): boolean { + return !isOptionalChainPunctuator(token); +} + +/** + * Returns true if and only if the node represents: foo?.() or foo.bar?.() + */ +function isOptionalOptionalChain( + node: TSESTree.Node, +): node is TSESTree.OptionalCallExpression { + return ( + node.type === AST_NODE_TYPES.OptionalCallExpression && + // this flag means the call expression itself is option + // i.e. it is foo.bar?.() and not foo?.bar() + node.optional + ); +} + +export { + LINEBREAK_MATCHER, + isNotOptionalChainPunctuator, + isOptionalChainPunctuator, + isOptionalOptionalChain, +}; diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 72213da6ebbf..2d0abcf963eb 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -215,3 +215,42 @@ export function typeIsOrHasBaseType( return false; } + +/** + * Gets the source file for a given node + */ +export function getSourceFileOfNode(node: ts.Node): ts.SourceFile { + while (node && node.kind !== ts.SyntaxKind.SourceFile) { + node = node.parent; + } + return node as ts.SourceFile; +} + +export function getTokenAtPosition( + sourceFile: ts.SourceFile, + position: number, +): ts.Node { + const queue: ts.Node[] = [sourceFile]; + let current: ts.Node; + while (queue.length > 0) { + current = queue.shift()!; + // find the child that contains 'position' + for (const child of current.getChildren(sourceFile)) { + const start = child.getFullStart(); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + return current; + } + + const end = child.getEnd(); + if ( + position < end || + (position === end && child.kind === ts.SyntaxKind.EndOfFileToken) + ) { + queue.push(child); + break; + } + } + } + return current!; +} diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.json b/packages/eslint-plugin/tests/fixtures/tsconfig.json index bedda0e3decc..92694993c539 100644 --- a/packages/eslint-plugin/tests/fixtures/tsconfig.json +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs", "strict": true, "esModuleInterop": true, - "lib": ["es2015", "es2017", "esnext"] + "lib": ["es2015", "es2017", "esnext"], + "experimentalDecorators": true } } diff --git a/packages/eslint-plugin/tests/rules/camelcase.test.ts b/packages/eslint-plugin/tests/rules/camelcase.test.ts index d2d3287ce662..617cdf3e40f7 100644 --- a/packages/eslint-plugin/tests/rules/camelcase.test.ts +++ b/packages/eslint-plugin/tests/rules/camelcase.test.ts @@ -79,6 +79,100 @@ ruleTester.run('camelcase', rule, { code: 'abstract class Foo { abstract bar: number = 0; }', options: [{ properties: 'always' }], }, + { + code: 'interface Foo {}', + options: [{ genericType: 'never' }], + }, + { + code: 'interface Foo {}', + options: [{ genericType: 'always' }], + }, + { + code: 'interface Foo {}', + options: [{ genericType: 'always' }], + }, + { + code: 'function fn() {}', + options: [{ genericType: 'never' }], + }, + { + code: 'function fn() {}', + options: [{ genericType: 'always' }], + }, + { + code: 'function fn() {}', + options: [{ genericType: 'always' }], + }, + { + code: 'class Foo {}', + options: [{ genericType: 'never' }], + }, + { + code: 'class Foo {}', + options: [{ genericType: 'always' }], + }, + { + code: 'class Foo {}', + options: [{ genericType: 'always' }], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'never' }], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'always' }], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'always' }], + }, + { + code: ` +type Foo = {} + `, + options: [{ genericType: 'always' }], + }, + { + code: ` +type Foo = {} + `, + options: [{ genericType: 'always' }], + }, + { + code: ` +type Foo = {} + `, + options: [{ genericType: 'never' }], + }, + { + code: ` +class Foo { + FOO_method() {} +} + `, + options: [{ allow: ['^FOO'] }], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{}], + }, { code: 'const foo = foo?.baz;', }, @@ -238,5 +332,117 @@ ruleTester.run('camelcase', rule, { }, ], }, + { + code: 'interface Foo {}', + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 1, + column: 15, + }, + ], + }, + { + code: 'function fn() {}', + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 1, + column: 13, + }, + ], + }, + { + code: 'class Foo {}', + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 1, + column: 11, + }, + ], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 3, + column: 10, + }, + ], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 3, + column: 10, + }, + { + messageId: 'notCamelCase', + data: { + name: 't_bar', + }, + line: 3, + column: 24, + }, + ], + }, + { + code: ` +class Foo { + method() {} +} + `, + options: [{ genericType: 'always' }], + errors: [ + { + messageId: 'notCamelCase', + data: { + name: 't_foo', + }, + line: 3, + column: 10, + }, + { + messageId: 'notCamelCase', + data: { + name: 't_bar', + }, + line: 3, + column: 18, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 71f51492d23d..f885d503091f 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -31,11 +31,15 @@ new print({ bar: 5 } as Foo) function foo() { throw { bar: 5 } as Foo } function b(x = {} as Foo.Bar) {} function c(x = {} as Foo) {} +print?.({ bar: 5 } as Foo) +print?.call({ bar: 5 } as Foo) `; const OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS = ` print({ bar: 5 }) new print({ bar: 5 }) function foo() { throw { bar: 5 } } +print?.({ bar: 5 }) +print?.call({ bar: 5 }) `; ruleTester.run('consistent-type-assertions', rule, { @@ -279,6 +283,14 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'unexpectedObjectTypeAssertion', line: 7, }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 8, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 9, + }, ], }), ...batchedSingleLineTests({ @@ -306,6 +318,14 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'unexpectedObjectTypeAssertion', line: 5, }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 6, + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 7, + }, ], }), ], diff --git a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts index 3552c718d65a..fb353eec1a37 100644 --- a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts @@ -266,6 +266,22 @@ foo(() => '') { filename: 'test.ts', code: ` +declare function foo(arg: () => void): void +foo?.(() => 1) +foo?.bar(() => {}) +foo?.bar?.(() => null) +foo.bar?.(() => true) +foo?.(() => '') + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` class Accumulator { private count: number = 0; diff --git a/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts b/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts index 14184b3fe0f4..e2250716a075 100644 --- a/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts @@ -33,6 +33,39 @@ ruleTester.run('func-call-spacing', rule, { '( f )( 0 )', '( (f) )( (0) )', '( f()() )(0)', + + // optional call + 'f?.();', + 'f?.(a, b);', + 'f?.b();', + 'f?.b()?.c();', + 'f.b?.();', + 'f.b?.().c();', + 'f()?.()', + '(function() {}?.())', + 'f?.( (0) )', + '(function(){ if (foo) { bar(); } }?.());', + 'f?.(0, (1))', + "describe/**/?.('foo', function () {});", + "describe?./**/('foo', function () {});", + '( f )?.( 0 )', + '( (f) )?.( (0) )', + '( f?.()() )(0)', + '( f()?.() )(0)', + '( f?.()?.() )(0)', + '( f?.()() )?.(0)', + '( f()?.() )?.(0)', + '( f?.()?.() )?.(0)', + 'f?.()', + 'f?.(b, b)', + 'f.b?.(b, b)', + 'f?.b(b, b)', + 'f?.b?.(b, b)', + '(function() {}?.())', + '((function() {})())', + '( f )?.( 0 )', + '( (f) )?.( (0) )', + '( f()() )?.(0)', ].map>(code => ({ code, options: ['never'], @@ -60,6 +93,11 @@ ruleTester.run('func-call-spacing', rule, { '( f ) ( 0 )', '( (f) ) ( (0) )', '( f () ) (0)', + + // optional call + 'f?.b ();', + 'f?.b ()?.c ();', + 'f?.b (b, b)', ].map>(code => ({ code, options: ['always'], @@ -76,6 +114,10 @@ ruleTester.run('func-call-spacing', rule, { 'f\u2028();', 'f\u2029();', 'f\r\n();', + + // optional call + 'f?.b \n ();', + 'f\n() ()?.b \n()\n ()', ].map>(code => ({ code, options: ['always', { allowNewlines: true }], @@ -364,5 +406,41 @@ var a = foo ...code, } as TSESLint.InvalidTestCase), ), + + // optional chain + ...[ + 'f ?.();', + 'f?. ();', + 'f ?. ();', + 'f\n?.();', + 'f?.\n();', + 'f\n?.\n();', + ].reduce[]>((acc, code) => { + acc.push( + { + options: ['always', { allowNewlines: true }], + errors: [{ messageId: 'unexpected' }], + code, + // apply no fixers to it + output: null, + }, + { + options: ['always'], + errors: [{ messageId: 'unexpected' }], + code, + // apply no fixers to it + output: null, + }, + { + options: ['never'], + errors: [{ messageId: 'unexpected' }], + code, + // apply no fixers to it + output: null, + }, + ); + + return acc; + }, []), ], }); diff --git a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts index 6b8570bb4a45..ca3da767f80c 100644 --- a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts @@ -24,6 +24,18 @@ ruleTester.run('no-array-constructor', rule, { 'new Array()', 'Array(1, 2, 3)', 'Array()', + + // optional chain + 'Array?.(x)', + 'Array?.(9)', + 'foo?.Array()', + 'Array?.foo()', + 'foo.Array?.()', + 'Array.foo?.()', + 'Array?.(1, 2, 3)', + 'Array?.()', + 'Array?.(0, 1, 2)', + 'Array?.(x, y)', ], invalid: [ diff --git a/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts b/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts index e3b4e5366e8b..aa12c33c9e50 100644 --- a/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts @@ -52,6 +52,8 @@ ruleTester.run('no-dynamic-delete', rule, { code: `const container: { [i: string]: 0 } = {}; delete container['aa' + 'b'];`, errors: [{ messageId: 'dynamicDelete' }], + output: `const container: { [i: string]: 0 } = {}; + delete container['aa' + 'b'];`, }, { code: `const container: { [i: string]: 0 } = {}; @@ -64,16 +66,22 @@ ruleTester.run('no-dynamic-delete', rule, { code: `const container: { [i: string]: 0 } = {}; delete container[-Infinity];`, errors: [{ messageId: 'dynamicDelete' }], + output: `const container: { [i: string]: 0 } = {}; + delete container[-Infinity];`, }, { code: `const container: { [i: string]: 0 } = {}; delete container[+Infinity];`, errors: [{ messageId: 'dynamicDelete' }], + output: `const container: { [i: string]: 0 } = {}; + delete container[+Infinity];`, }, { code: `const container: { [i: string]: 0 } = {}; delete container[NaN];`, errors: [{ messageId: 'dynamicDelete' }], + output: `const container: { [i: string]: 0 } = {}; + delete container[NaN];`, }, { code: `const container: { [i: string]: 0 } = {}; @@ -94,11 +102,26 @@ ruleTester.run('no-dynamic-delete', rule, { const name = 'name'; delete container[name];`, errors: [{ messageId: 'dynamicDelete' }], + output: `const container: { [i: string]: 0 } = {}; + const name = 'name'; + delete container[name];`, }, { code: `const container: { [i: string]: 0 } = {}; const getName = () => 'aaa'; delete container[getName()];`, + output: `const container: { [i: string]: 0 } = {}; + const getName = () => 'aaa'; + delete container[getName()];`, + errors: [{ messageId: 'dynamicDelete' }], + }, + { + code: `const container: { [i: string]: 0 } = {}; + const name = { foo: { bar: 'bar' } }; + delete container[name.foo.bar];`, + output: `const container: { [i: string]: 0 } = {}; + const name = { foo: { bar: 'bar' } }; + delete container[name.foo.bar];`, errors: [{ messageId: 'dynamicDelete' }], }, ], diff --git a/packages/eslint-plugin/tests/rules/no-empty-interface.test.ts b/packages/eslint-plugin/tests/rules/no-empty-interface.test.ts index 6a9f62deb68d..f02d753dee1e 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-interface.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-interface.test.ts @@ -73,5 +73,63 @@ interface Bar extends Foo {} }, ], }, + { + code: 'interface Foo extends Array {}', + output: 'type Foo = Array', + errors: [ + { + messageId: 'noEmptyWithSuper', + line: 1, + column: 11, + }, + ], + }, + { + code: 'interface Foo extends Array { }', + output: 'type Foo = Array', + errors: [ + { + messageId: 'noEmptyWithSuper', + line: 1, + column: 11, + }, + ], + }, + { + code: ` +interface Bar { + bar: string; +} +interface Foo extends Array {} +`, + output: ` +interface Bar { + bar: string; +} +type Foo = Array +`, + errors: [ + { + messageId: 'noEmptyWithSuper', + line: 5, + column: 11, + }, + ], + }, + { + code: ` +type R = Record; +interface Foo extends R { };`, + output: ` +type R = Record; +type Foo = R;`, + errors: [ + { + messageId: 'noEmptyWithSuper', + line: 3, + column: 11, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts index c5624ba06351..0bc8e5f3438f 100644 --- a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts +++ b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts @@ -3,6 +3,7 @@ import { RuleTester } from '../RuleTester'; import { TSESLint } from '@typescript-eslint/experimental-utils'; type InvalidTestCase = TSESLint.InvalidTestCase; +type SuggestionOutput = TSESLint.SuggestionOutput; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -306,11 +307,31 @@ type obj = { messageId: 'unexpectedAny', line: 1, column: 31, + suggestions: [ + { + messageId: 'suggestUnknown', + output: 'function generic(param: Array): Array {}', + }, + { + messageId: 'suggestNever', + output: 'function generic(param: Array): Array {}', + }, + ], }, { messageId: 'unexpectedAny', line: 1, column: 44, + suggestions: [ + { + messageId: 'suggestUnknown', + output: 'function generic(param: Array): Array {}', + }, + { + messageId: 'suggestNever', + output: 'function generic(param: Array): Array {}', + }, + ], }, ], }, @@ -705,11 +726,31 @@ type obj = { messageId: 'unexpectedAny', line: 1, column: 15, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `class Foo extends Bar {}`, + }, + { + messageId: 'suggestNever', + output: `class Foo extends Bar {}`, + }, + ], }, { messageId: 'unexpectedAny', line: 1, column: 32, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `class Foo extends Bar {}`, + }, + { + messageId: 'suggestNever', + output: `class Foo extends Bar {}`, + }, + ], }, ], }, @@ -720,11 +761,31 @@ type obj = { messageId: 'unexpectedAny', line: 1, column: 24, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `abstract class Foo extends Bar {}`, + }, + { + messageId: 'suggestNever', + output: `abstract class Foo extends Bar {}`, + }, + ], }, { messageId: 'unexpectedAny', line: 1, column: 41, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `abstract class Foo extends Bar {}`, + }, + { + messageId: 'suggestNever', + output: `abstract class Foo extends Bar {}`, + }, + ], }, ], }, @@ -735,16 +796,46 @@ type obj = { messageId: 'unexpectedAny', line: 1, column: 24, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `abstract class Foo implements Bar, Baz {}`, + }, + { + messageId: 'suggestNever', + output: `abstract class Foo implements Bar, Baz {}`, + }, + ], }, { messageId: 'unexpectedAny', line: 1, column: 44, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `abstract class Foo implements Bar, Baz {}`, + }, + { + messageId: 'suggestNever', + output: `abstract class Foo implements Bar, Baz {}`, + }, + ], }, { messageId: 'unexpectedAny', line: 1, column: 54, + suggestions: [ + { + messageId: 'suggestUnknown', + output: `abstract class Foo implements Bar, Baz {}`, + }, + { + messageId: 'suggestNever', + output: `abstract class Foo implements Bar, Baz {}`, + }, + ], }, ], }, @@ -771,19 +862,51 @@ type obj = { { // https://github.com/typescript-eslint/typescript-eslint/issues/64 code: ` - function test>() {} - const test = >() => {}; - `, +function test>() {} +const test = >() => {}; + `.trimRight(), errors: [ { messageId: 'unexpectedAny', line: 2, - column: 41, + column: 33, + suggestions: [ + { + messageId: 'suggestUnknown', + output: ` +function test>() {} +const test = >() => {}; + `.trimRight(), + }, + { + messageId: 'suggestNever', + output: ` +function test>() {} +const test = >() => {}; + `.trimRight(), + }, + ], }, { messageId: 'unexpectedAny', line: 3, - column: 41, + column: 33, + suggestions: [ + { + messageId: 'suggestUnknown', + output: ` +function test>() {} +const test = >() => {}; + `.trimRight(), + }, + { + messageId: 'suggestNever', + output: ` +function test>() {} +const test = >() => {}; + `.trimRight(), + }, + ], }, ], }, @@ -869,7 +992,23 @@ type obj = { ], }, ] as InvalidTestCase[]).reduce((acc, testCase) => { - acc.push(testCase); + const suggestions = (code: string): SuggestionOutput[] => [ + { + messageId: 'suggestUnknown', + output: code.replace(/any/, 'unknown'), + }, + { + messageId: 'suggestNever', + output: code.replace(/any/, 'never'), + }, + ]; + acc.push({ + ...testCase, + errors: testCase.errors.map(e => ({ + ...e, + suggestions: e.suggestions ?? suggestions(testCase.code), + })), + }); const options = testCase.options || []; const code = `// fixToUnknown: true\n${testCase.code}`; acc.push({ @@ -881,7 +1020,17 @@ type obj = { return err; } - return { ...err, line: err.line + 1 }; + return { + ...err, + line: err.line + 1, + suggestions: + err.suggestions?.map( + (s): SuggestionOutput => ({ + ...s, + output: `// fixToUnknown: true\n${s.output}`, + }), + ) ?? suggestions(code), + }; }), }); diff --git a/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts b/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts new file mode 100644 index 000000000000..a851fe9a0604 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-extra-non-null-assertion.test.ts @@ -0,0 +1,66 @@ +import rule from '../../src/rules/no-extra-non-null-assertion'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-extra-non-null-assertion', rule, { + valid: [ + { + code: ` +const foo: { bar: number } | null = null; +const bar = foo!.bar; + `, + }, + { + code: ` +function foo(bar: number | undefined) { + const bar: number = bar!; +} `, + }, + ], + invalid: [ + { + code: ` +const foo: { bar: number } | null = null; +const bar = foo!!!!.bar; + `, + errors: [ + { + messageId: 'noExtraNonNullAssertion', + endColumn: 19, + column: 13, + line: 3, + }, + { + messageId: 'noExtraNonNullAssertion', + endColumn: 18, + column: 13, + line: 3, + }, + { + messageId: 'noExtraNonNullAssertion', + endColumn: 17, + column: 13, + line: 3, + }, + ], + }, + { + code: ` +function foo(bar: number | undefined) { + const bar: number = bar!!; +} + `, + errors: [ + { + messageId: 'noExtraNonNullAssertion', + endColumn: 27, + column: 23, + line: 3, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts b/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts index 1d2742ed0402..e009aa153ae8 100644 --- a/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts +++ b/packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts @@ -65,6 +65,13 @@ export class Bar { }, // https://github.com/typescript-eslint/typescript-eslint/issues/170 'export default class { hello() { return "I am foo!"; } }', + { + code: ` +@FooDecorator +class Foo {} + `, + options: [{ allowWithDecorator: true }], + }, ], invalid: [ @@ -125,5 +132,17 @@ export class AClass { }, ], }, + { + code: ` +@FooDecorator +class Foo {} + `, + options: [{ allowWithDecorator: false }], + errors: [ + { + messageId: 'empty', + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index 013edb41bc0e..d381fb4ba7c1 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -219,6 +219,18 @@ async function test() { return promise; } `, + + // optional chaining + ` +async function test() { + declare const returnsPromise: () => Promise | null; + await returnsPromise?.(); + returnsPromise()?.then(() => {}, () => {}); + returnsPromise()?.then(() => {})?.catch(() => {}); + returnsPromise()?.catch(() => {}); + return returnsPromise(); +} + `, ], invalid: [ diff --git a/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts b/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts index 2301d6eda5c2..050e7bee4168 100644 --- a/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts +++ b/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts @@ -15,11 +15,18 @@ function flatten(arr: T[][]): T[] { const testCases = [ { type: 'bigint', - code: ['10n', '-10n', 'BigInt(10)', '-BigInt(10)'], + code: [ + '10n', + '-10n', + 'BigInt(10)', + '-BigInt(10)', + 'BigInt?.(10)', + '-BigInt?.(10)', + ], }, { type: 'boolean', - code: ['false', 'true', 'Boolean(null)', '!0'], + code: ['false', 'true', 'Boolean(null)', 'Boolean?.(null)', '!0'], }, { type: 'number', @@ -30,6 +37,9 @@ const testCases = [ 'Number("1")', '+Number("1")', '-Number("1")', + 'Number?.("1")', + '+Number?.("1")', + '-Number?.("1")', 'Infinity', '+Infinity', '-Infinity', @@ -44,15 +54,15 @@ const testCases = [ }, { type: 'RegExp', - code: ['/a/', 'RegExp("a")', 'new RegExp("a")'], + code: ['/a/', 'RegExp("a")', 'RegExp?.("a")', 'new RegExp("a")'], }, { type: 'string', - code: ['"str"', "'str'", '`str`', 'String(1)'], + code: ['"str"', "'str'", '`str`', 'String(1)', 'String?.(1)'], }, { type: 'symbol', - code: ['Symbol("a")'], + code: ['Symbol("a")', 'Symbol?.("a")'], }, { type: 'undefined', diff --git a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts index 5d4c7fcf4b85..fb18a3875e34 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-promises.test.ts @@ -105,7 +105,15 @@ if (value) {} ` const fn: (arg: () => Promise | void) => void = () => {}; fn(() => Promise.resolve()); -`, + `, + ` +declare const returnsPromise : (() => Promise) | null; +if (returnsPromise?.()) {} + `, + ` +declare const returnsPromise : { call: (() => Promise) } | null; +if (returnsPromise?.call()) {} + `, ], invalid: [ @@ -263,6 +271,42 @@ const fnWithCallback = (arg: string, cb: (err: any, res: string) => void) => { cb(null, arg); }; +fnWithCallback('val', (err, res) => { + if (err) { + return 'abc'; + } else { + return Promise.resolve(res); + } +}); +`, + errors: [ + { + line: 6, + messageId: 'voidReturn', + }, + ], + }, + { + code: ` +const fnWithCallback: ((arg: string, cb: (err: any, res: string) => void) => void) | null = (arg, cb) => { + cb(null, arg); +}; + +fnWithCallback?.('val', (err, res) => Promise.resolve(res)); +`, + errors: [ + { + line: 6, + messageId: 'voidReturn', + }, + ], + }, + { + code: ` +const fnWithCallback: ((arg: string, cb: (err: any, res: string) => void) => void) | null = (arg, cb) => { + cb(null, arg); +}; + fnWithCallback('val', (err, res) => { if (err) { return 'abc'; diff --git a/packages/eslint-plugin/tests/rules/no-require-imports.test.ts b/packages/eslint-plugin/tests/rules/no-require-imports.test.ts index fbaf1f47faea..ef49e4df1bf4 100644 --- a/packages/eslint-plugin/tests/rules/no-require-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/no-require-imports.test.ts @@ -16,6 +16,7 @@ ruleTester.run('no-require-imports', rule, { 'var lib7 = 700', 'import lib9 = lib2.anotherSubImport', "import lib10 from 'lib10'", + "var lib3 = load?.('not_an_import')", ], invalid: [ { @@ -63,5 +64,40 @@ ruleTester.run('no-require-imports', rule, { }, ], }, + { + code: "var lib = require?.('lib')", + errors: [ + { + messageId: 'noRequireImports', + line: 1, + column: 11, + }, + ], + }, + { + code: "let lib2 = require?.('lib2')", + errors: [ + { + messageId: 'noRequireImports', + line: 1, + column: 12, + }, + ], + }, + { + code: "var lib5 = require?.('lib5'), lib6 = require?.('lib6')", + errors: [ + { + messageId: 'noRequireImports', + line: 1, + column: 12, + }, + { + messageId: 'noRequireImports', + line: 1, + column: 38, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index c5d90fd01efa..30e511ac119d 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -97,6 +97,14 @@ declare const b2: true; if(b1 && b2) {}`, options: [{ ignoreRhs: true }], }, + { + code: ` +while(true) {} +for (;true;) {} +do {} while(true) + `, + options: [{ allowConstantLoopConditions: true }], + }, ], invalid: [ // Ensure that it's checking in all the right places @@ -201,5 +209,18 @@ const t1 = (b1 && b2) ? 'yes' : 'no'`, ruleError(9, 13, 'alwaysTruthy'), ], }, + { + code: ` +while(true) {} +for (;true;) {} +do {} while(true) + `, + options: [{ allowConstantLoopConditions: false }], + errors: [ + ruleError(2, 7, 'alwaysTruthy'), + ruleError(3, 7, 'alwaysTruthy'), + ruleError(4, 13, 'alwaysTruthy'), + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts index 05794ee30f41..b22d123cbcb5 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts @@ -18,6 +18,10 @@ ruleTester.run('no-unnecessary-type-arguments', rule, { f();`, `function f() { } f();`, + `declare const f: (() => void) | null; + f?.();`, + `declare const f: (() => void) | null; + f?.();`, `declare const f: any; f();`, `declare const f: any; diff --git a/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts b/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts index 1a167d64e30d..164f11b4bd76 100644 --- a/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts +++ b/packages/eslint-plugin/tests/rules/no-untyped-public-signature.test.ts @@ -28,7 +28,7 @@ ruleTester.run('no-untyped-public-signature', rule, { code: ` class A { public b(c: string):void { - + } }`, }, @@ -36,7 +36,7 @@ ruleTester.run('no-untyped-public-signature', rule, { code: ` class A { public b(...c):void { - + } }`, }, @@ -44,7 +44,7 @@ ruleTester.run('no-untyped-public-signature', rule, { code: ` class A { b(c):void { - + } }`, options: [{ ignoredMethods: ['b'] }], @@ -71,22 +71,43 @@ ruleTester.run('no-untyped-public-signature', rule, { code: ` class A { b(...c):void { - + } - + d(c):void { - + } }`, options: [{ ignoredMethods: ['b', 'd'] }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/1229 + ` +class Foo { + constructor() {} +} + `, + ` +class Foo { + abstract constructor() {} +} + `, + ` +class Foo { + constructor(c: string) {} +} + `, + ` +class Foo { + abstract constructor(c: string) {} +} + `, ], invalid: [ //untyped parameter { code: `class A { public b(c):void { - + } }`, errors: [{ messageId: 'untypedParameter' }], @@ -206,5 +227,22 @@ ruleTester.run('no-untyped-public-signature', rule, { }`, errors: [{ messageId: 'noReturnType' }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/1229 + { + code: ` +class Foo { + constructor(c) {} +} + `, + errors: [{ messageId: 'untypedParameter' }], + }, + { + code: ` +class Foo { + abstract constructor(c) {} +} + `, + errors: [{ messageId: 'untypedParameter' }], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts new file mode 100644 index 000000000000..6cb2a24e5a3a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts @@ -0,0 +1,1361 @@ +import { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/experimental-utils/dist/ts-eslint'; +import rule, { + DEFAULT_IGNORED_REGEX_STRING, + Options, + MessageIds, +} from '../../src/rules/no-unused-vars-experimental'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + parser: '@typescript-eslint/parser', +}); + +const hasExport = /^export/m; +// const hasImport = /^import .+? from ['"]/m; +function makeExternalModule< + T extends ValidTestCase | InvalidTestCase +>(tests: T[]): T[] { + tests.forEach(t => { + if (!hasExport.test(t.code)) { + t.code = `${t.code}\nexport const __externalModule = 1;`; + } + }); + return tests; +} + +const DEFAULT_IGNORED_REGEX = new RegExp( + DEFAULT_IGNORED_REGEX_STRING, +).toString(); +ruleTester.run('no-unused-vars-experimental', rule, { + valid: makeExternalModule([ + /////////////////////// + // #region variables // + /////////////////////// + { code: 'const _x = "unused"' }, + { code: 'export const x = "used";' }, + { + code: ` +const x = "used"; +console.log(x); + `, + }, + { + code: ` +function foo() {} +foo(); + `, + }, + { code: 'function _foo() {}' }, + { + // decorators require the tsconfig compiler option + // or else they are marked as unused because it is not a valid usage + code: ` +function decorator(_clazz: any) {} + +@decorator +export class Foo {} + `, + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + }, + { + code: ` +type Foo = { a?: string }; +export const foo: Foo = {}; + `, + }, + { + code: ` +interface Foo { a?: string }; +export const foo: Foo = {}; + `, + }, + { code: 'type _Foo = { a?: string };' }, + { code: 'interface _Foo { a?: string };' }, + { + code: ` +class Foo {} +new Foo(); + `, + }, + { code: 'class _Foo {}' }, + { + code: ` +export class Foo { + private foo: string; + bar() { + console.log(this.foo); + } +} + `, + }, + { + code: ` +export class Foo { + private _foo: string; +} + `, + }, + { + code: ` +export class Foo { + private foo() {}; + bar() { + this.foo(); + } +} + `, + }, + { + code: ` +export class Foo { + private _foo() {}; +} + `, + }, + { + code: ` +enum Foo { a = 1 } +console.log(Foo.a); + `, + }, + { code: 'enum _Foo { a = 1 }' }, + { code: 'export const {a, b} = c;' }, + { + code: ` +const {a, b: {c}} = d; +console.log(a, c); + `, + }, + { + code: ` +const {a, b} = c; +console.log(a, b); + `, + }, + { + code: ` +const {a: _a, b} = c; +console.log(b); + `, + }, + { code: `const {a: _a, b: _b} = c;` }, + { code: 'export const [a, b] = c;' }, + { + code: ` +const [a, b] = c; +console.log(a, b); + `, + }, + { + code: ` +const [a, [b]] = c; +console.log(a, b); + `, + }, + { + code: ` +const [_a, b] = c; +console.log(b); + `, + }, + { code: `const [_a, _b] = c;` }, + // #endregion variables // + ////////////////////////// + + //////////////////////// + // #region parameters // + //////////////////////// + { + code: ` +export function foo(a) { + console.log(a); +} + `, + }, + { + code: ` +export function foo(a: string, b: string) { + console.log(b); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, public b: string) {} +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(private a: string, public b: string) {} +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(private a: string) {} + foo() { console.log(this.a) } +} + `, + }, + { code: 'export function foo({a: _a}) {}' }, + { code: 'export function foo({a: { b: _b }}) {}' }, + { code: 'export function foo([_a]) {}' }, + { code: 'export function foo([[_a]]) {}' }, + { + code: ` +export function foo({a: _a}, used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo({a: { b: _b }}, used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo([_a], used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo([[_a]], used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + // #endregion parameters // + /////////////////////////// + + //////////////////// + // #region import // + //////////////////// + { + code: ` +import defaultImp from "thing"; +console.log(defaultImp); + `, + }, + { + code: ` +import { named } from "thing"; +console.log(named); + `, + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(defaultImp, named); + `, + }, + { + code: ` +import defaultImp = require("thing"); +console.log(defaultImp, named); + `, + }, + { + code: ` +import * as namespace from "thing"; +console.log(namespace); + `, + }, + { + code: ` +import defaultImp, * as namespace from "thing"; +console.log(defaultImp, namespace); + `, + }, + { code: 'import _defaultImp from "thing";' }, + { code: 'import { named as _named } from "thing";' }, + { code: 'import _defaultImp, { named as _named } from "thing";' }, + { code: 'import _defaultImp = require("thing");' }, + { code: 'import * as _namespace from "thing";' }, + { code: 'import _defaultImp, * as _namespace from "thing";' }, + // #endregion import // + /////////////////////// + + ////////////////////// + // #region generics // + ////////////////////// + { code: 'export function foo(): T {}' }, + { code: 'export function foo(): T & T2 {}' }, + { code: 'export function foo(): T {}' }, + { + code: ` +export class foo { + prop: T +} + `, + }, + { + code: ` +export class foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export class foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export interface foo { + prop: T +} + `, + }, + { + code: ` +export interface foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export interface foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export type foo = { + prop: T + +} + `, + }, + { + code: ` +export type foo = { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export type foo = { + prop: T + prop2: T2 +} + `, + }, + // #endregion generics // + ///////////////////////// + ]), + invalid: makeExternalModule([ + /////////////////////// + // #region variables // + /////////////////////// + { + code: 'const x = "unused"', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'x', + type: 'Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 8, + }, + ], + }, + { + code: 'const x: string = "unused"', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'x', + type: 'Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 16, + }, + ], + }, + { + code: 'const x = "unused"', + options: [ + { + ignoredNamesRegex: false, + }, + ], + errors: [ + { + messageId: 'unused', + data: { + name: 'x', + type: 'Variable', + }, + line: 1, + column: 7, + endColumn: 8, + }, + ], + }, + { + code: 'function foo() {}', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Function', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 10, + endColumn: 13, + }, + ], + }, + { + code: 'type Foo = { a?: string };', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Type', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 6, + endColumn: 9, + }, + ], + }, + { + code: 'interface Foo { a?: string };', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Interface', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: 'class Foo {}', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Class', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 10, + }, + ], + }, + { + code: ` +export class Foo { + private foo: string; +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Property', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 3, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: ` +export class Foo { + private foo() {}; +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Method', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 3, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: 'enum Foo { a = 1 }', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Enum', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 6, + endColumn: 9, + }, + ], + }, + { + code: ` +const {foo, bar} = baz; +console.log(foo); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: ` +const [foo, bar] = baz; +console.log(foo); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const {foo, bar} = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const {foo, bar: _bar} = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'const [foo, bar] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const [foo, _bar] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'const [foo, [bar]] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 14, + endColumn: 17, + }, + ], + }, + { + code: 'const {foo, bar: {baz}} = bam;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'baz', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 19, + endColumn: 22, + }, + ], + }, + { + code: 'const {foo, bar: [baz]} = bam;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'baz', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 19, + endColumn: 22, + }, + ], + }, + // #endregion variables // + ////////////////////////// + + //////////////////////// + // #region parameters // + //////////////////////// + { + code: ` +export function foo(a, b) { + console.log(b); +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 2, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: ` +export function foo(a: string, b: string) { + console.log(b); +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 2, + column: 21, + endColumn: 30, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, b: string) { + console.log(b); + } +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 3, + column: 15, + endColumn: 24, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, public b: string) {} +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 3, + column: 15, + endColumn: 24, + }, + ], + }, + { + code: ` +export function foo({a}, used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: ` +export function foo({a: {b}}, used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'b', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 26, + endColumn: 27, + }, + ], + }, + { + code: ` +export function foo([a], used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: ` +export function foo([[a]], used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 23, + endColumn: 24, + }, + ], + }, + // #endregion parameters // + /////////////////////////// + + //////////////////// + // #region import // + //////////////////// + { + code: 'import foo = require("test")', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'import defaultImp from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 32, + }, + ], + }, + { + code: 'import { named } from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 31, + }, + ], + }, + { + code: 'import * as namespace from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 36, + }, + ], + }, + { + code: 'import defaultImp, { named } from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 43, + }, + ], + }, + { + code: 'import defaultImp, * as namespace from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 48, + }, + ], + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(named); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'defaultImp', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 8, + endColumn: 18, + }, + ], + }, + { + code: ` +import defaultImp, * as named from "thing"; +console.log(named); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'defaultImp', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 8, + endColumn: 18, + }, + ], + }, + { + code: ` +import defaultImp, * as named from "thing"; +console.log(defaultImp); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 25, + endColumn: 30, + }, + ], + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(defaultImp); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: ` +import { named1, named2 } from "thing"; +console.log(named1); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named2', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 18, + endColumn: 24, + }, + ], + }, + // #endregion import // + /////////////////////// + + ////////////////////// + // #region generics // + ////////////////////// + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 20, + endColumn: 23, + }, + ], + }, + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 20, + endColumn: 27, + }, + ], + }, + { + code: 'export function foo(): T2 {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 17, + endColumn: 20, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 17, + endColumn: 24, + }, + ], + }, + { + code: ` +export class foo { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 18, + endColumn: 19, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 18, + endColumn: 19, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 21, + endColumn: 24, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 21, + endColumn: 28, + }, + ], + }, + { + code: ` +export interface foo { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 16, + endColumn: 19, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 16, + endColumn: 23, + }, + ], + }, + { + code: ` +export type foo = { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 17, + endColumn: 18, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 17, + endColumn: 18, + }, + ], + }, + // #endregion generics // + ///////////////////////// + ]), +}); diff --git a/packages/eslint-plugin/tests/rules/no-var-requires.test.ts b/packages/eslint-plugin/tests/rules/no-var-requires.test.ts index b042ac1f8067..4433a634c7b4 100644 --- a/packages/eslint-plugin/tests/rules/no-var-requires.test.ts +++ b/packages/eslint-plugin/tests/rules/no-var-requires.test.ts @@ -6,7 +6,7 @@ const ruleTester = new RuleTester({ }); ruleTester.run('no-var-requires', rule, { - valid: ["import foo = require('foo')", "require('foo')"], + valid: ["import foo = require('foo')", "require('foo')", "require?.('foo')"], invalid: [ { code: "var foo = require('foo')", @@ -48,5 +48,55 @@ ruleTester.run('no-var-requires', rule, { }, ], }, + { + code: "var foo = require?.('foo')", + errors: [ + { + messageId: 'noVarReqs', + line: 1, + column: 11, + }, + ], + }, + { + code: "const foo = require?.('foo')", + errors: [ + { + messageId: 'noVarReqs', + line: 1, + column: 13, + }, + ], + }, + { + code: "let foo = require?.('foo')", + errors: [ + { + messageId: 'noVarReqs', + line: 1, + column: 11, + }, + ], + }, + { + code: "let foo = trick(require?.('foo'))", + errors: [ + { + messageId: 'noVarReqs', + line: 1, + column: 17, + }, + ], + }, + { + code: "let foo = trick?.(require('foo'))", + errors: [ + { + messageId: 'noVarReqs', + line: 1, + column: 19, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/prefer-for-of.test.ts b/packages/eslint-plugin/tests/rules/prefer-for-of.test.ts index db4dcd3bbf91..4eb08c7e9455 100644 --- a/packages/eslint-plugin/tests/rules/prefer-for-of.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-for-of.test.ts @@ -149,6 +149,32 @@ for (let i = 0; i < arr.length; i++) { ({ foo: arr[i] }) = { foo: 0 }; } `, + ` +for (let i = 0; i < arr1?.length; i++) { + const x = arr1[i] === arr2[i]; +} + `, + ` +for (let i = 0; i < arr?.length; i++) { + arr[i] = 0; +} + `, + ` +for (var c = 0; c < arr?.length; c++) { + doMath(c); +} + `, + ` +for (var d = 0; d < arr?.length; d++) doMath(d); + `, + ` +for (var c = 0; c < arr.length; c++) { + doMath?.(c); +} + `, + ` +for (var d = 0; d < arr.length; d++) doMath?.(d); + `, ], invalid: [ { @@ -177,6 +203,28 @@ for (var b = 0; b < arr.length; b++) console.log(arr[b]); code: ` for (let a = 0; a < arr.length; a++) { console.log(arr[a]); +} + `, + errors: [ + { + messageId: 'preferForOf', + }, + ], + }, + { + code: ` +for (var b = 0; b < arr.length; b++) console?.log(arr[b]); + `, + errors: [ + { + messageId: 'preferForOf', + }, + ], + }, + { + code: ` +for (let a = 0; a < arr.length; a++) { + console?.log(arr[a]); } `, errors: [ diff --git a/packages/eslint-plugin/tests/rules/prefer-includes.test.ts b/packages/eslint-plugin/tests/rules/prefer-includes.test.ts index f754c4875211..6094f3adee69 100644 --- a/packages/eslint-plugin/tests/rules/prefer-includes.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-includes.test.ts @@ -1,9 +1,13 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; import path from 'path'; import rule from '../../src/rules/prefer-includes'; +import * as util from '../../src/util'; import { RuleTester } from '../RuleTester'; const rootPath = path.join(process.cwd(), 'tests/fixtures/'); +type MessageIds = util.InferMessageIdsTypeFromRule; + const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { @@ -12,8 +16,31 @@ const ruleTester = new RuleTester({ }, }); +type InvalidTestCase = TSESLint.InvalidTestCase; +type ValidTestCase = TSESLint.ValidTestCase | string; +function addOptional(cases: ValidTestCase[]): ValidTestCase[]; +function addOptional(cases: InvalidTestCase[]): InvalidTestCase[]; +function addOptional( + cases: (ValidTestCase | InvalidTestCase)[], +): (ValidTestCase | InvalidTestCase)[] { + return cases.reduce<(ValidTestCase | InvalidTestCase)[]>((acc, c) => { + acc.push(c); + if (typeof c === 'string') { + acc.push(c.replace('.', '?.')); + } else { + acc.push({ + ...c, + code: c.code.replace('.', '?.'), + output: 'output' in c ? c.output?.replace('.', '?.') : null, + }); + } + + return acc; + }, []); +} + ruleTester.run('prefer-includes', rule, { - valid: [ + valid: addOptional([ ` function f(a: string): void { a.indexOf(b) @@ -89,8 +116,8 @@ ruleTester.run('prefer-includes', rule, { something.test(a) } `, - ], - invalid: [ + ]), + invalid: addOptional([ // positive { code: ` @@ -435,5 +462,5 @@ ruleTester.run('prefer-includes', rule, { `, errors: [{ messageId: 'preferIncludes' }], }, - ], + ]), }); diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts new file mode 100644 index 000000000000..1f05d6b9a3fc --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -0,0 +1,439 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import path from 'path'; +import rule, { + MessageIds, + Options, +} from '../../src/rules/prefer-nullish-coalescing'; +import { RuleTester } from '../RuleTester'; + +const rootPath = path.join(process.cwd(), 'tests/fixtures/'); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +const types = ['string', 'number', 'boolean', 'object']; +const nullishTypes = ['null', 'undefined', 'null | undefined']; + +function typeValidTest( + cb: (type: string) => TSESLint.ValidTestCase | string, +): (TSESLint.ValidTestCase | string)[] { + return types.map(type => cb(type)); +} +function nullishTypeValidTest( + cb: ( + nullish: string, + type: string, + ) => TSESLint.ValidTestCase | string, +): (TSESLint.ValidTestCase | string)[] { + return nullishTypes.reduce<(TSESLint.ValidTestCase | string)[]>( + (acc, nullish) => { + types.forEach(type => { + acc.push(cb(nullish, type)); + }); + return acc; + }, + [], + ); +} +function nullishTypeInvalidTest( + cb: ( + nullish: string, + type: string, + ) => TSESLint.InvalidTestCase, +): TSESLint.InvalidTestCase[] { + return nullishTypes.reduce[]>( + (acc, nullish) => { + types.forEach(type => { + acc.push(cb(nullish, type)); + }); + return acc; + }, + [], + ); +} + +ruleTester.run('prefer-nullish-coalescing', rule, { + valid: [ + ...typeValidTest( + type => ` +declare const x: ${type}; +x || 'foo'; + `, + ), + ...nullishTypeValidTest( + (nullish, type) => ` +declare const x: ${type} | ${nullish}; +x ?? 'foo'; + `, + ), + + // ignoreConditionalTests + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +x || 'foo' ? null : null; + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +if (x || 'foo') {} + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +do {} while (x || 'foo') + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +for (;x || 'foo';) {} + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +while (x || 'foo') {} + `, + options: [{ ignoreConditionalTests: true }], + })), + + // ignoreMixedLogicalExpressions + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +a || b && c; + `, + options: [{ ignoreMixedLogicalExpressions: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a || b || c && d; + `, + options: [{ ignoreMixedLogicalExpressions: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a && b || c || d; + `, + options: [{ ignoreMixedLogicalExpressions: true }], + })), + ], + invalid: [ + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +x || 'foo'; + `, + output: ` +declare const x: ${type} | ${nullish}; +x ?? 'foo'; + `, + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 3, + endLine: 3, + endColumn: 5, + }, + ], + })), + + // ignoreConditionalTests + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +x || 'foo' ? null : null; + `, + output: ` +declare const x: ${type} | ${nullish}; +x ?? 'foo' ? null : null; + `, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 3, + endLine: 3, + endColumn: 5, + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +if (x || 'foo') {} + `, + output: ` +declare const x: ${type} | ${nullish}; +if (x ?? 'foo') {} + `, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 7, + endLine: 3, + endColumn: 9, + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +do {} while (x || 'foo') + `, + output: ` +declare const x: ${type} | ${nullish}; +do {} while (x ?? 'foo') + `, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 16, + endLine: 3, + endColumn: 18, + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +for (;x || 'foo';) {} + `, + output: ` +declare const x: ${type} | ${nullish}; +for (;x ?? 'foo';) {} + `, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 9, + endLine: 3, + endColumn: 11, + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +while (x || 'foo') {} + `, + output: ` +declare const x: ${type} | ${nullish}; +while (x ?? 'foo') {} + `, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 10, + endLine: 3, + endColumn: 12, + }, + ], + })), + + // ignoreMixedLogicalExpressions + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +a || b && c; + `.trimRight(), + options: [{ ignoreMixedLogicalExpressions: false }], + errors: [ + { + messageId: 'preferNullish', + line: 5, + column: 3, + endLine: 5, + endColumn: 5, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +a ?? b && c; + `.trimRight(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a || b || c && d; + `.trimRight(), + options: [{ ignoreMixedLogicalExpressions: false }], + errors: [ + { + messageId: 'preferNullish', + line: 6, + column: 3, + endLine: 6, + endColumn: 5, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a ?? b || c && d; + `.trimRight(), + }, + ], + }, + { + messageId: 'preferNullish', + line: 6, + column: 8, + endLine: 6, + endColumn: 10, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a || b ?? c && d; + `.trimRight(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a && b || c || d; + `.trimRight(), + options: [{ ignoreMixedLogicalExpressions: false }], + errors: [ + { + messageId: 'preferNullish', + line: 6, + column: 8, + endLine: 6, + endColumn: 10, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a && b ?? c || d; + `.trimRight(), + }, + ], + }, + { + messageId: 'preferNullish', + line: 6, + column: 13, + endLine: 6, + endColumn: 15, + suggestions: [ + { + messageId: 'preferNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type} | ${nullish}; +declare const c: ${type} | ${nullish}; +declare const d: ${type} | ${nullish}; +a && b || c ?? d; + `.trimRight(), + }, + ], + }, + ], + })), + + // should not false postivie for functions inside conditional tests + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +if (() => x || 'foo') {} + `, + output: ` +declare const x: ${type} | ${nullish}; +if (() => x ?? 'foo') {} + `, + options: [{ ignoreConditionalTests: true }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 13, + endLine: 3, + endColumn: 15, + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const x: ${type} | ${nullish}; +if (function werid() { return x || 'foo' }) {} + `, + output: ` +declare const x: ${type} | ${nullish}; +if (function werid() { return x ?? 'foo' }) {} + `, + options: [{ ignoreConditionalTests: true }], + errors: [ + { + messageId: 'preferNullish', + line: 3, + column: 33, + endLine: 3, + endColumn: 35, + }, + ], + })), + ], +}); diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts new file mode 100644 index 000000000000..2cadf43883e7 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -0,0 +1,225 @@ +import rule from '../../src/rules/prefer-optional-chain'; +import { RuleTester } from '../RuleTester'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, +} from '../../src/util'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const baseCases = [ + // chained members + { + code: ` + foo && foo.bar + `, + output: ` + foo?.bar + `, + }, + { + code: ` + foo && foo() + `, + output: ` + foo?.() + `, + }, + { + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz + `, + output: ` + foo?.bar?.baz?.buzz + `, + }, + { + // case with a jump (i.e. a non-nullish prop) + code: ` + foo && foo.bar && foo.bar.baz.buzz + `, + output: ` + foo?.bar?.baz.buzz + `, + }, + { + // case where for some reason there is a doubled up expression + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz && foo.bar.baz.buzz + `, + output: ` + foo?.bar?.baz?.buzz + `, + }, + // chained members with element access + { + code: ` + foo && foo[bar] && foo[bar].baz && foo[bar].baz.buzz + `, + output: ` + foo?.[bar]?.baz?.buzz + `, + }, + { + // case with a jump (i.e. a non-nullish prop) + code: ` + foo && foo[bar].baz && foo[bar].baz.buzz + `, + output: ` + foo?.[bar].baz?.buzz + `, + }, + // chained calls + { + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz() + `, + output: ` + foo?.bar?.baz?.buzz() + `, + }, + { + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz && foo.bar.baz.buzz() + `, + output: ` + foo?.bar?.baz?.buzz?.() + `, + }, + { + // case with a jump (i.e. a non-nullish prop) + code: ` + foo && foo.bar && foo.bar.baz.buzz() + `, + output: ` + foo?.bar?.baz.buzz() + `, + }, + { + // case with a jump (i.e. a non-nullish prop) + code: ` + foo && foo.bar && foo.bar.baz.buzz && foo.bar.baz.buzz() + `, + output: ` + foo?.bar?.baz.buzz?.() + `, + }, + { + // case with a call expr inside the chain for some inefficient reason + code: ` + foo && foo.bar() && foo.bar().baz && foo.bar().baz.buzz && foo.bar().baz.buzz() + `, + output: ` + foo?.bar()?.baz?.buzz?.() + `, + }, + // chained calls with element access + { + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz]() + `, + output: ` + foo?.bar?.baz?.[buzz]() + `, + }, + { + code: ` + foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz] && foo.bar.baz[buzz]() + `, + output: ` + foo?.bar?.baz?.[buzz]?.() + `, + }, + // two-for-one + { + code: ` + foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo + `, + output: ` + foo?.bar?.baz || baz?.bar?.foo + `, + errors: 2, + }, +].map( + c => + ({ + code: c.code.trim(), + output: c.output.trim(), + errors: Array(c.errors ?? 1).fill({ + messageId: 'preferOptionalChain', + }), + } as TSESLint.InvalidTestCase< + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule + >), +); + +ruleTester.run('prefer-optional-chain', rule, { + valid: [ + 'foo && bar', + 'foo && foo', + 'foo || bar', + 'foo ?? bar', + 'foo || foo.bar', + 'foo ?? foo.bar', + "file !== 'index.ts' && file.endsWith('.ts')", + 'nextToken && sourceCode.isSpaceBetweenTokens(prevToken, nextToken)', + ], + invalid: [ + ...baseCases, + // it should ignore whitespace in the expressions + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/\./g, '. '), + })), + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/\./g, '.\n'), + })), + // it should ignore parts of the expression that aren't part of the expression chain + ...baseCases.map(c => ({ + ...c, + code: `${c.code} && bing`, + output: `${c.output} && bing`, + })), + ...baseCases.map(c => ({ + ...c, + code: `${c.code} && bing.bong`, + output: `${c.output} && bing.bong`, + })), + // strict nullish equality checks x !== null && x.y !== null + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/&&/g, ' !== null &&'), + })), + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/&&/g, ' != null &&'), + })), + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/&&/g, ' !== undefined &&'), + })), + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/&&/g, ' != undefined &&'), + })), + { + // case with inconsistent checks + code: ` + foo && foo.bar != null && foo.bar.baz && foo.bar.baz.buzz !== undefined + `, + output: ` + foo?.bar?.baz?.buzz + `, + errors: [ + { + messageId: 'preferOptionalChain', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/require-await.test.ts b/packages/eslint-plugin/tests/rules/require-await.test.ts index 09b54bd3a99d..fff294f7ccb5 100644 --- a/packages/eslint-plugin/tests/rules/require-await.test.ts +++ b/packages/eslint-plugin/tests/rules/require-await.test.ts @@ -28,106 +28,87 @@ const noAwaitAsyncFunctionExpression: any = { ruleTester.run('require-await', rule, { valid: [ - { - // Non-async function declaration - code: `function numberOne(): number { - return 1; - }`, - }, - { - // Non-async function expression - code: `const numberOne = function(): number { - return 1; - }`, - }, - { - // Non-async arrow function expression (concise-body) - code: `const numberOne = (): number => 1;`, - }, - { - // Non-async arrow function expression (block-body) - code: `const numberOne = (): number => { - return 1; - };`, - }, - { - // Async function declaration with await - code: `async function numberOne(): Promise { - return await 1; - }`, - }, - { - // Async function expression with await - code: `const numberOne = async function(): Promise { - return await 1; - }`, - }, - { - // Async arrow function expression with await (concise-body) - code: `const numberOne = async (): Promise => await 1;`, - }, - { - // Async arrow function expression with await (block-body) - code: `const numberOne = async (): Promise => { - return await 1; - };`, - }, - { - // Async function declaration with promise return - code: `async function numberOne(): Promise { - return Promise.resolve(1); - }`, - }, - { - // Async function expression with promise return - code: `const numberOne = async function(): Promise { - return Promise.resolve(1); - }`, - }, - { - // Async arrow function with promise return (concise-body) - code: `const numberOne = async (): Promise => Promise.resolve(1);`, - }, - { - // Async arrow function with promise return (block-body) - code: `const numberOne = async (): Promise => { - return Promise.resolve(1); - };`, - }, - { - // Async function declaration with async function return - code: `async function numberOne(): Promise { - return getAsyncNumber(1); - } - async function getAsyncNumber(x: number): Promise { - return Promise.resolve(x); - }`, - }, - { - // Async function expression with async function return - code: `const numberOne = async function(): Promise { - return getAsyncNumber(1); - } - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, - }, - { - // Async arrow function with async function return (concise-body) - code: `const numberOne = async (): Promise => getAsyncNumber(1); - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, - }, - { - // Async arrow function with async function return (block-body) - code: `const numberOne = async (): Promise => { - return getAsyncNumber(1); - }; - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, - }, + // Non-async function declaration + `function numberOne(): number { + return 1; + }`, + // Non-async function expression + `const numberOne = function(): number { + return 1; + }`, + // Non-async arrow function expression (concise-body) + `const numberOne = (): number => 1;`, + // Non-async arrow function expression (block-body) + `const numberOne = (): number => { + return 1; + };`, + // Non-async function that returns a promise + // https://github.com/typescript-eslint/typescript-eslint/issues/1226 + ` +function delay() { + return Promise.resolve(); +} + `, + ` +const delay = () => { + return Promise.resolve(); +} + `, + `const delay = () => Promise.resolve();`, + // Async function declaration with await + `async function numberOne(): Promise { + return await 1; + }`, + // Async function expression with await + `const numberOne = async function(): Promise { + return await 1; + }`, + // Async arrow function expression with await (concise-body) + `const numberOne = async (): Promise => await 1;`, + // Async arrow function expression with await (block-body) + `const numberOne = async (): Promise => { + return await 1; + };`, + // Async function declaration with promise return + `async function numberOne(): Promise { + return Promise.resolve(1); + }`, + // Async function expression with promise return + `const numberOne = async function(): Promise { + return Promise.resolve(1); + }`, + // Async arrow function with promise return (concise-body) + `const numberOne = async (): Promise => Promise.resolve(1);`, + // Async arrow function with promise return (block-body) + `const numberOne = async (): Promise => { + return Promise.resolve(1); + };`, + // Async function declaration with async function return + `async function numberOne(): Promise { + return getAsyncNumber(1); + } + async function getAsyncNumber(x: number): Promise { + return Promise.resolve(x); + }`, + // Async function expression with async function return + `const numberOne = async function(): Promise { + return getAsyncNumber(1); + } + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, + // Async arrow function with async function return (concise-body) + `const numberOne = async (): Promise => getAsyncNumber(1); + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, + // Async arrow function with async function return (block-body) + `const numberOne = async (): Promise => { + return getAsyncNumber(1); + }; + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, // https://github.com/typescript-eslint/typescript-eslint/issues/1188 ` async function testFunction(): Promise { diff --git a/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts b/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts index 44583a202a1d..5d979706f84d 100644 --- a/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts +++ b/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts @@ -81,6 +81,28 @@ function foo(a: T) { return a + 1; } `, + { + code: ` +let foo: number = 0; +foo += 1; + `, + options: [ + { + checkCompoundAssignments: false, + }, + ], + }, + { + code: ` +let foo: number = 0; +foo += "string"; + `, + options: [ + { + checkCompoundAssignments: false, + }, + ], + }, ], invalid: [ { @@ -383,5 +405,41 @@ function foo(a: T) { }, ], }, + { + code: ` +let foo: string | undefined; +foo += "some data"; + `, + options: [ + { + checkCompoundAssignments: true, + }, + ], + errors: [ + { + messageId: 'notStrings', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +let foo = ''; +foo += 0; + `, + options: [ + { + checkCompoundAssignments: true, + }, + ], + errors: [ + { + messageId: 'notStrings', + line: 3, + column: 1, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts new file mode 100644 index 000000000000..bddbc85d22c0 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -0,0 +1,383 @@ +import rule from '../../src/rules/return-await'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +// default rule is in-try-catch +ruleTester.run('return-await', rule, { + valid: [ + `function test() { + return; + }`, + `function test() { + return 1; + }`, + `async function test() { + return; + }`, + `async function test() { + return 1; + }`, + `const test = () => 1;`, + `const test = async () => 1;`, + `async function test() { + return Promise.resolve(1); + }`, + `async function test() { + try { + return await Promise.resolve(1); + } catch (e) { + return await Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + `async function test() { + try { + const one = await Promise.resolve(1); + return one; + } catch (e) { + const two = await Promise.resolve(2); + return two; + } finally { + console.log('cleanup'); + } + }`, + { + options: ['in-try-catch'], + code: `function test() { + return 1; + }`, + }, + { + options: ['in-try-catch'], + code: `async function test() { + return 1; + }`, + }, + { + options: ['in-try-catch'], + code: `const test = () => 1;`, + }, + { + options: ['in-try-catch'], + code: `const test = async () => 1;`, + }, + { + options: ['in-try-catch'], + code: `async function test() { + return Promise.resolve(1); + }`, + }, + { + options: ['in-try-catch'], + code: `async function test() { + try { + return await Promise.resolve(1); + } catch (e) { + return await Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + }, + { + options: ['in-try-catch'], + code: `async function test() { + try { + const one = await Promise.resolve(1); + return one; + } catch (e) { + const two = await Promise.resolve(2); + return two; + } finally { + console.log('cleanup'); + } + }`, + }, + { + options: ['never'], + code: `async function test() { + return Promise.resolve(1); + }`, + }, + { + options: ['never'], + code: `const test = async () => Promise.resolve(1);`, + }, + { + options: ['never'], + code: `async function test() { + try { + return Promise.resolve(1); + } catch (e) { + return Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + }, + { + options: ['always'], + code: `async function test() { + return await Promise.resolve(1); + }`, + }, + { + options: ['always'], + code: `const test = async () => await Promise.resolve(1);`, + }, + { + options: ['always'], + code: `async function test() { + try { + return await Promise.resolve(1); + } catch (e) { + return await Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + }, + ], + invalid: [ + { + code: `async function test() { + return await 1; + }`, + errors: [ + { + line: 2, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + code: `const test = async () => await 1;`, + errors: [ + { + line: 1, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + code: `const test = async () => await Promise.resolve(1);`, + errors: [ + { + line: 1, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + code: `async function test() { + try { + return Promise.resolve(1); + } catch (e) { + return Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + errors: [ + { + line: 3, + messageId: 'requiredPromiseAwait', + }, + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + code: `async function test() { + return await Promise.resolve(1); + }`, + errors: [ + { + line: 2, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + options: ['in-try-catch'], + code: `async function test() { + return await 1; + }`, + errors: [ + { + line: 2, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + options: ['in-try-catch'], + code: `const test = async () => await 1;`, + errors: [ + { + line: 1, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + options: ['in-try-catch'], + code: `const test = async () => await Promise.resolve(1);`, + errors: [ + { + line: 1, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + options: ['in-try-catch'], + code: `async function test() { + try { + return Promise.resolve(1); + } catch (e) { + return Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + errors: [ + { + line: 3, + messageId: 'requiredPromiseAwait', + }, + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + options: ['in-try-catch'], + code: `async function test() { + return await Promise.resolve(1); + }`, + errors: [ + { + line: 2, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + options: ['never'], + code: `async function test() { + return await 1; + }`, + errors: [ + { + line: 2, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + options: ['never'], + code: `async function test() { + try { + return await Promise.resolve(1); + } catch (e) { + return await Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + errors: [ + { + line: 3, + messageId: 'disallowedPromiseAwait', + }, + { + line: 5, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + options: ['never'], + code: `async function test() { + return await Promise.resolve(1); + }`, + errors: [ + { + line: 2, + messageId: 'disallowedPromiseAwait', + }, + ], + }, + { + options: ['always'], + code: `async function test() { + return await 1; + }`, + errors: [ + { + line: 2, + messageId: 'nonPromiseAwait', + }, + ], + }, + { + options: ['always'], + code: `async function test() { + try { + return Promise.resolve(1); + } catch (e) { + return Promise.resolve(2); + } finally { + console.log('cleanup'); + } + }`, + errors: [ + { + line: 3, + messageId: 'requiredPromiseAwait', + }, + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + options: ['always'], + code: `async function test() { + return Promise.resolve(1); + }`, + errors: [ + { + line: 2, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + options: ['always'], + code: `const test = async () => Promise.resolve(1);`, + errors: [ + { + line: 1, + messageId: 'requiredPromiseAwait', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts b/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts index ed6a8131edba..cd5828a8e36f 100644 --- a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts +++ b/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts @@ -37,8 +37,12 @@ function validateTableStructure( console.error( chalk.bold.red('✗'), chalk.bold('Sorting:'), - 'Incorrect line number for', + 'Incorrect row index for', chalk.bold(rowRuleName), + 'expected', + ruleIndex, + 'got', + rowIndex, ); hasErrors = true; return; diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 8037e624c4f2..62f45419b188 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -35,6 +35,7 @@ declare module 'eslint/lib/rules/camelcase' { allow?: string[]; ignoreDestructuring?: boolean; properties?: 'always' | 'never'; + genericType?: 'never' | 'always'; }, ], { @@ -451,7 +452,7 @@ declare module 'eslint/lib/rules/require-await' { node: TSESTree.ArrowFunctionExpression, ): void; ReturnStatement(node: TSESTree.ReturnStatement): void; - AwaitExpression(node: TSESTree.AwaitExpression): void; + AwaitExpression(): void; ForOfStatement(node: TSESTree.ForOfStatement): void; } >; diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 719fea3d87b8..fcacc1bd5071 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + + +### Features + +* suggestion types, suggestions for no-explicit-any ([#1250](https://github.com/typescript-eslint/typescript-eslint/issues/1250)) ([b16a4b6](https://github.com/typescript-eslint/typescript-eslint/commit/b16a4b6)) +* **eslint-plugin:** add prefer-nullish-coalescing ([#1069](https://github.com/typescript-eslint/typescript-eslint/issues/1069)) ([a9cd399](https://github.com/typescript-eslint/typescript-eslint/commit/a9cd399)) +* **eslint-plugin:** add rule prefer-optional-chain ([#1213](https://github.com/typescript-eslint/typescript-eslint/issues/1213)) ([ad7e1a7](https://github.com/typescript-eslint/typescript-eslint/commit/ad7e1a7)) + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) **Note:** Version bump only for package @typescript-eslint/experimental-utils diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 5fef538da56c..5960b669d597 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.8.0", + "version": "2.9.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -37,7 +37,7 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.8.0", + "@typescript-eslint/typescript-estree": "2.9.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts b/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts index f8080ca87ffa..6ed25e4a8ac4 100644 --- a/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts +++ b/packages/experimental-utils/src/eslint-utils/batchedSingleLineTests.ts @@ -57,7 +57,7 @@ function batchedSingleLineTests< line: 1, })), }; - if (output && output[i]) { + if (output?.[i]) { return { ...returnVal, output: output[i], diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index dad91e6a8e09..c74358da5587 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -105,6 +105,9 @@ interface RuleFixer { type ReportFixFunction = ( fixer: RuleFixer, ) => null | RuleFix | RuleFix[] | IterableIterator; +type ReportSuggestionArray = Readonly< + ReportDescriptorBase[] +>; interface ReportDescriptorBase { /** @@ -119,7 +122,19 @@ interface ReportDescriptorBase { * The messageId which is being reported. */ messageId: TMessageIds; + // we disallow this because it's much better to use messageIds for reusable errors that are easily testable + // message?: string; + // suggestions instead have this property that works the same, but again it's much better to use messageIds + // desc?: string; } +interface ReportDescriptorWithSuggestion + extends ReportDescriptorBase { + /** + * 6.7's Suggestions API + */ + suggest?: ReportSuggestionArray | null; +} + interface ReportDescriptorNodeOptionalLoc { /** * The Node or AST Token which the report is being attached to @@ -136,9 +151,9 @@ interface ReportDescriptorLocOnly { */ loc: TSESTree.SourceLocation | TSESTree.LineAndColumnData; } -type ReportDescriptor = ReportDescriptorBase< - TMessageIds -> & +type ReportDescriptor< + TMessageIds extends string +> = ReportDescriptorWithSuggestion & (ReportDescriptorNodeOptionalLoc | ReportDescriptorLocOnly); interface RuleContext< @@ -283,6 +298,8 @@ interface RuleListener { NewExpression?: RuleFunction; ObjectExpression?: RuleFunction; ObjectPattern?: RuleFunction; + OptionalCallExpression?: RuleFunction; + OptionalMemberExpression?: RuleFunction; Program?: RuleFunction; Property?: RuleFunction; RestElement?: RuleFunction; @@ -416,6 +433,7 @@ interface RuleModule< export { ReportDescriptor, ReportFixFunction, + ReportSuggestionArray, RuleContext, RuleFix, RuleFixer, diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index 84da228fb81e..c59574589b45 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -19,6 +19,16 @@ interface ValidTestCase> { }; } +interface SuggestionOutput { + messageId: TMessageIds; + data?: Record; + /** + * NOTE: Suggestions will be applied as a stand-alone change, without triggering multipass fixes. + * Each individual error has its own suggestion, so you have to show the correct, _isolated_ output for each suggestion. + */ + output: string; +} + interface InvalidTestCase< TMessageIds extends string, TOptions extends Readonly @@ -35,6 +45,7 @@ interface TestCaseError { column?: number; endLine?: number; endColumn?: number; + suggestions?: SuggestionOutput[]; } interface RunTests< @@ -61,7 +72,7 @@ class RuleTester extends (ESLintRuleTester as { // nobody will ever need watching in tests // so we can give everyone a perf win by disabling watching - if (config && config.parserOptions && config.parserOptions.project) { + if (config?.parserOptions?.project) { config.parserOptions.noWatch = typeof config.parserOptions.noWatch === 'boolean' || true; } @@ -79,6 +90,7 @@ class RuleTester extends (ESLintRuleTester as { export { InvalidTestCase, + SuggestionOutput, RuleTester, RuleTesterConfig, RunTests, diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index cd1688d72e5b..7153621c2f2f 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) diff --git a/packages/parser/package.json b/packages/parser/package.json index d494917898c8..41b4ee671951 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.8.0", + "version": "2.9.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,13 +43,13 @@ }, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.8.0", - "@typescript-eslint/typescript-estree": "2.8.0", + "@typescript-eslint/experimental-utils": "2.9.0", + "@typescript-eslint/typescript-estree": "2.9.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.8.0", + "@typescript-eslint/shared-fixtures": "2.9.0", "glob": "*" } } diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index 67d553fac465..db9088604340 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -380,8 +380,8 @@ class Referencer extends TSESLintScope.Referencer { // Ignore this if other overloadings have already existed. if (id) { const variable = upperScope.set.get(id.name); - const defs = variable && variable.defs; - const existed = defs && defs.some(d => d.type === 'FunctionName'); + const defs = variable?.defs; + const existed = defs?.some((d): boolean => d.type === 'FunctionName'); if (!existed) { upperScope.__define( id, diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index 4b84a123ae34..32efd0513156 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index dd930d1575d1..d7e303ae1a6b 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.8.0", + "version": "2.9.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index 8500277af65c..27973aa5246d 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.8.0...v2.9.0) (2019-11-25) + + +### Bug Fixes + +* **typescript-estree:** fix synthetic default import ([#1245](https://github.com/typescript-eslint/typescript-eslint/issues/1245)) ([d97f809](https://github.com/typescript-eslint/typescript-eslint/commit/d97f809)) + + +### Features + +* **eslint-plugin:** add no-unused-vars-experimental ([#688](https://github.com/typescript-eslint/typescript-eslint/issues/688)) ([05ebea5](https://github.com/typescript-eslint/typescript-eslint/commit/05ebea5)) + + + + + # [2.8.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.7.0...v2.8.0) (2019-11-18) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index c169fad437e6..8a0857d19af5 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.8.0", + "version": "2.9.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -59,7 +59,7 @@ "@types/lodash.unescape": "^4.0.4", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.8.0", + "@typescript-eslint/shared-fixtures": "2.9.0", "babel-code-frame": "^6.26.0", "lodash.isplainobject": "4.0.6", "tmp": "^0.1.0", diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 67ca0be79b89..c5dd74449551 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -277,8 +277,7 @@ export class Converter { const child = this.convertChild(statement); if (allowDirectives) { if ( - child && - child.expression && + child?.expression && ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression) ) { @@ -1427,10 +1426,9 @@ export class Converter { body: [], range: [node.members.pos - 1, node.end], }), - superClass: - superClass && superClass.types[0] - ? this.convertChild(superClass.types[0].expression) - : null, + superClass: superClass?.types[0] + ? this.convertChild(superClass.types[0].expression) + : null, }); if (superClass) { diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 38bd209ff3f2..03624dfd2266 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -322,7 +322,7 @@ function createWatchProgram( return null; } - return oldSetTimeout && oldSetTimeout(cb, ms, ...args); + return oldSetTimeout?.(cb, ms, ...args); }; return ts.createWatchProgram(watchCompilerHost); diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index a19bef274139..1ba44449867d 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -16,6 +16,8 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { checkJs: true, noEmit: true, // extendedDiagnostics: true, + noUnusedLocals: true, + noUnusedParameters: true, }; // This narrows the type so we can be sure we're passing canonical names in the correct places diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 0ef765ed8b64..27a10024dc8b 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -282,7 +282,7 @@ function parse( /** * Ensure users do not attempt to use parse() when they need parseAndGenerateServices() */ - if (options && options.errorOnTypeScriptSyntacticAndSemanticIssues) { + if (options?.errorOnTypeScriptSyntacticAndSemanticIssues) { throw new Error( `"errorOnTypeScriptSyntacticAndSemanticIssues" is only supported for parseAndGenerateServices()`, ); diff --git a/packages/typescript-estree/src/visitor-keys.ts b/packages/typescript-estree/src/visitor-keys.ts index e594fb240e05..d7ee3ed356fc 100644 --- a/packages/typescript-estree/src/visitor-keys.ts +++ b/packages/typescript-estree/src/visitor-keys.ts @@ -1,4 +1,4 @@ -import eslintVisitorKeys from 'eslint-visitor-keys'; +import * as eslintVisitorKeys from 'eslint-visitor-keys'; export const visitorKeys = eslintVisitorKeys.unionWith({ // Additional estree nodes. diff --git a/packages/typescript-estree/tests/ast-alignment/utils.ts b/packages/typescript-estree/tests/ast-alignment/utils.ts index 563fd19cc309..3051bfceb65c 100644 --- a/packages/typescript-estree/tests/ast-alignment/utils.ts +++ b/packages/typescript-estree/tests/ast-alignment/utils.ts @@ -28,7 +28,7 @@ export function omitDeep( nodes: Record void> = {}, ): any { function shouldOmit(keyName: string, val: any): boolean { - if (keysToOmit && keysToOmit.length) { + if (keysToOmit?.length) { return keysToOmit.some( keyConfig => keyConfig.key === keyName && keyConfig.predicate(val), ); diff --git a/tests/integration/README.md b/tests/integration/README.md index eb33e8bd62df..c0aa0b976433 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -14,5 +14,5 @@ These tests are setup to run within docker containers to ensure that each test i 1. Copy+paste the `test.sh` from an existing fixture, and adjust the `eslint` command as required. 1. Add a new entry to `docker-compose.yml` by copy+pasting an existing section, and changing the name to match your new folder. 1. Add a new entry to `run-all-tests.sh` by copy+pasting an existing command, and changing the name to match your new folder. -1. Run your integration test by running the single command you copied in . +1. Run your integration test by running the single command you copied in the previous step. - If your test finishes successfully, a `test.js.snap` will be created. diff --git a/tests/integration/fixtures/markdown/test.js.snap b/tests/integration/fixtures/markdown/test.js.snap index 4fc96f79abc7..5f28d2474ea6 100644 --- a/tests/integration/fixtures/markdown/test.js.snap +++ b/tests/integration/fixtures/markdown/test.js.snap @@ -29,6 +29,30 @@ Array [ "nodeType": "TSAnyKeyword", "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, + "suggestions": Array [ + Object { + "desc": "Use \`unknown\` instead, this will force you to explicitly, and safely assert the type is correct.", + "fix": Object { + "range": Array [ + 51, + 54, + ], + "text": "unknown", + }, + "messageId": "suggestUnknown", + }, + Object { + "desc": "Use \`never\` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + "fix": Object { + "range": Array [ + 51, + 54, + ], + "text": "never", + }, + "messageId": "suggestNever", + }, + ], }, Object { "column": 3, @@ -62,6 +86,30 @@ Array [ "nodeType": "TSAnyKeyword", "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, + "suggestions": Array [ + Object { + "desc": "Use \`unknown\` instead, this will force you to explicitly, and safely assert the type is correct.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "unknown", + }, + "messageId": "suggestUnknown", + }, + Object { + "desc": "Use \`never\` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "never", + }, + "messageId": "suggestNever", + }, + ], }, Object { "column": 3, @@ -84,6 +132,30 @@ Array [ "nodeType": "TSAnyKeyword", "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, + "suggestions": Array [ + Object { + "desc": "Use \`unknown\` instead, this will force you to explicitly, and safely assert the type is correct.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "unknown", + }, + "messageId": "suggestUnknown", + }, + Object { + "desc": "Use \`never\` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "never", + }, + "messageId": "suggestNever", + }, + ], }, Object { "column": 3, @@ -106,6 +178,30 @@ Array [ "nodeType": "TSAnyKeyword", "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, + "suggestions": Array [ + Object { + "desc": "Use \`unknown\` instead, this will force you to explicitly, and safely assert the type is correct.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "unknown", + }, + "messageId": "suggestUnknown", + }, + Object { + "desc": "Use \`never\` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + "fix": Object { + "range": Array [ + 16, + 19, + ], + "text": "never", + }, + "messageId": "suggestNever", + }, + ], }, Object { "column": 3, diff --git a/tests/integration/fixtures/vue-jsx/test.js.snap b/tests/integration/fixtures/vue-jsx/test.js.snap index d4432e47428a..e0fbff509d49 100644 --- a/tests/integration/fixtures/vue-jsx/test.js.snap +++ b/tests/integration/fixtures/vue-jsx/test.js.snap @@ -18,6 +18,30 @@ Array [ "nodeType": "TSAnyKeyword", "ruleId": "@typescript-eslint/no-explicit-any", "severity": 2, + "suggestions": Array [ + Object { + "desc": "Use \`unknown\` instead, this will force you to explicitly, and safely assert the type is correct.", + "fix": Object { + "range": Array [ + 390, + 393, + ], + "text": "unknown", + }, + "messageId": "suggestUnknown", + }, + Object { + "desc": "Use \`never\` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + "fix": Object { + "range": Array [ + 390, + 393, + ], + "text": "never", + }, + "messageId": "suggestNever", + }, + ], }, ], "source": "