diff --git a/.all-contributorsrc b/.all-contributorsrc
index 55cf18e201b1..1c5c3a5adcdb 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -100,6 +100,13 @@
"profile": "https://github.com/Retsam",
"contributions": []
},
+ {
+ "login": "yeonjuan",
+ "name": "YeonJuan",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/41323220?v=4",
+ "profile": "https://github.com/yeonjuan",
+ "contributions": []
+ },
{
"login": "kaicataldo",
"name": "Kai Cataldo",
@@ -128,13 +135,6 @@
"profile": "https://github.com/azz",
"contributions": []
},
- {
- "login": "yeonjuan",
- "name": "YeonJuan",
- "avatar_url": "https://avatars3.githubusercontent.com/u/41323220?v=4",
- "profile": "https://github.com/yeonjuan",
- "contributions": []
- },
{
"login": "dannyfritz",
"name": "Danny Fritz",
@@ -177,6 +177,13 @@
"profile": "https://github.com/anikethsaha",
"contributions": []
},
+ {
+ "login": "octogonz",
+ "name": "Pete Gonzalez",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/4673363?v=4",
+ "profile": "https://github.com/octogonz",
+ "contributions": []
+ },
{
"login": "ldrick",
"name": "Ricky Lippmann",
@@ -191,6 +198,13 @@
"profile": "https://github.com/SimenB",
"contributions": []
},
+ {
+ "login": "cherryblossom000",
+ "name": "cherryblossom000",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/31467609?v=4",
+ "profile": "https://github.com/cherryblossom000",
+ "contributions": []
+ },
{
"login": "vapurrmaid",
"name": "G r e y",
@@ -233,13 +247,6 @@
"profile": "https://github.com/pablobirukov",
"contributions": []
},
- {
- "login": "octogonz",
- "name": "Pete Gonzalez",
- "avatar_url": "https://avatars0.githubusercontent.com/u/4673363?v=4",
- "profile": "https://github.com/octogonz",
- "contributions": []
- },
{
"login": "mightyiam",
"name": "Shahar Dawn Or",
@@ -330,13 +337,6 @@
"avatar_url": "https://avatars2.githubusercontent.com/u/296735?v=4",
"profile": "https://github.com/madbence",
"contributions": []
- },
- {
- "login": "dependabot[bot]",
- "name": "dependabot[bot]",
- "avatar_url": "https://avatars0.githubusercontent.com/in/29110?v=4",
- "profile": "https://github.com/apps/dependabot",
- "contributions": []
}
],
"contributorsPerLine": 5
diff --git a/.cspell.json b/.cspell.json
index daf674b70f33..45e79f884857 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -77,6 +77,8 @@
"Premade",
"prettier's",
"recurse",
+ "redeclaration",
+ "redeclarations",
"redeclared",
"reimplement",
"resync",
@@ -92,7 +94,8 @@
"typedef",
"typedefs",
"unfixable",
- "unprefixed"
+ "unprefixed",
+ "Zacher"
],
"overrides": [
{
diff --git a/.eslintrc.js b/.eslintrc.js
index 6e70052cdfd3..5fbcecbe0667 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -214,6 +214,7 @@ module.exports = {
'packages/eslint-plugin-internal/tests/rules/**/*.test.ts',
'packages/eslint-plugin-tslint/tests/rules/**/*.test.ts',
'packages/eslint-plugin/tests/rules/**/*.test.ts',
+ 'packages/eslint-plugin/tests/eslint-rules/**/*.test.ts',
],
rules: {
'@typescript-eslint/internal/plugin-test-formatting': 'error',
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cfc7a1ea39e1..5d4ee278edb4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- master
+ # TODO - delete this before merging v4 into master
+ - v4
pull_request:
branches:
- '**'
@@ -348,3 +350,48 @@ jobs:
run: npx lerna publish --loglevel=verbose --canary --exact --force-publish --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ # TODO - delete this before merging v4 into master
+ publish_v4_prerelease_version:
+ name: Publish the latest code as a v4 prerelease version
+ runs-on: ubuntu-latest
+ needs: [typecheck, test_on_primary_node_version, unit_tests_on_other_node_versions, linting_and_style, integration_tests]
+ if: github.ref == 'refs/heads/v4'
+ steps:
+ - uses: actions/checkout@v2
+ # Fetch all history for all tags and branches in this job because lerna needs it
+ - run: |
+ git fetch --prune --unshallow
+
+ - name: Use Node.js ${{ env.PRIMARY_NODE_VERSION }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ env.PRIMARY_NODE_VERSION }}
+ registry-url: https://registry.npmjs.org/
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v1
+ id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Install dependencies
+ run: |
+ yarn --ignore-engines --frozen-lockfile --ignore-scripts
+ yarn lerna:init
+ yarn check:clean-workspace-after-install
+
+ - name: Build
+ run: |
+ yarn build
+
+ - name: Publish all packages to npm
+ run: npx lerna publish premajor --loglevel=verbose --canary --exact --force-publish --yes --dist-tag rc-v4
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cbbbd123a5f..8355e4ba6d45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [4.0.0](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.1...v4.0.0) (2020-08-31)
+
+
+### Bug Fixes
+
+* **eslint-plugin:** [no-shadow] fix false-positive on enum declaration ([#2374](https://github.com/typescript-eslint/typescript-eslint/issues/2374)) ([9de669f](https://github.com/typescript-eslint/typescript-eslint/commit/9de669f339fef62a98f745dc08b833aa5c632e62))
+* **eslint-plugin:** [no-unused-vars] handle TSCallSignature ([#2336](https://github.com/typescript-eslint/typescript-eslint/issues/2336)) ([c70f54f](https://github.com/typescript-eslint/typescript-eslint/commit/c70f54fd3a46a12060ae3aec0faae872c431dd88))
+* correct decorator traversal for AssignmentPattern ([#2375](https://github.com/typescript-eslint/typescript-eslint/issues/2375)) ([d738fa4](https://github.com/typescript-eslint/typescript-eslint/commit/d738fa4eff0a5c4cfc9b30b1c0502f8d1e78d7b6))
+* **scope-manager:** correct analysis of abstract class properties ([#2420](https://github.com/typescript-eslint/typescript-eslint/issues/2420)) ([cd84549](https://github.com/typescript-eslint/typescript-eslint/commit/cd84549beba3cf471d75cfd9ba26f80366842ed5))
+* **typescript-estree:** correct ChainExpression interaction with parentheses and non-nulls ([#2380](https://github.com/typescript-eslint/typescript-eslint/issues/2380)) ([762bc99](https://github.com/typescript-eslint/typescript-eslint/commit/762bc99584ede4d0b8099a743991e957aec86aa8))
+
+
+### Features
+
+* consume new scope analysis package ([#2039](https://github.com/typescript-eslint/typescript-eslint/issues/2039)) ([3be125d](https://github.com/typescript-eslint/typescript-eslint/commit/3be125d9bdbee1984ac6037874edf619213bd3d0))
+* support ESTree optional chaining representation ([#2308](https://github.com/typescript-eslint/typescript-eslint/issues/2308)) ([e9d2ab6](https://github.com/typescript-eslint/typescript-eslint/commit/e9d2ab638b6767700b52797e74b814ea059beaae))
+* **eslint-plugin:** [ban-ts-comment] change default for `ts-expect-error` to `allow-with-description` ([#2351](https://github.com/typescript-eslint/typescript-eslint/issues/2351)) ([a3f163a](https://github.com/typescript-eslint/typescript-eslint/commit/a3f163abc03f0fefc6dca1f205b728a4425209e4)), closes [#2146](https://github.com/typescript-eslint/typescript-eslint/issues/2146)
+* **eslint-plugin:** [no-unnecessary-condition][strict-boolean-expressions] add option to make the rules error on files without `strictNullChecks` turned on ([#2345](https://github.com/typescript-eslint/typescript-eslint/issues/2345)) ([9273441](https://github.com/typescript-eslint/typescript-eslint/commit/9273441f7592b52620e10432cb2dd4dc5c3b4db1))
+* **eslint-plugin:** [typedef] remove all defaults ([#2352](https://github.com/typescript-eslint/typescript-eslint/issues/2352)) ([a9cd6fb](https://github.com/typescript-eslint/typescript-eslint/commit/a9cd6fb893074e4f2ca9ad3497eaddfacb3cfd25))
+* **eslint-plugin:** add `consistent-type-imports` rule ([#2367](https://github.com/typescript-eslint/typescript-eslint/issues/2367)) ([58b1c2d](https://github.com/typescript-eslint/typescript-eslint/commit/58b1c2d463f34895798b9a61340e49ffc3ec4f1a))
+* **typescript-estree:** switch to globby ([#2418](https://github.com/typescript-eslint/typescript-eslint/issues/2418)) ([3a7ec9b](https://github.com/typescript-eslint/typescript-eslint/commit/3a7ec9bcf1873a99c6da2f19ade8ab4763b4793c)), closes [#2398](https://github.com/typescript-eslint/typescript-eslint/issues/2398)
+
+
+### BREAKING CHANGES
+
+* **typescript-estree:** - removes the ability to supply a `RegExp` to `projectFolderIgnoreList`, and changes the meaning of the string value from a regex to a glob.
+* - Removed decorators property from several Nodes that could never semantically have them (FunctionDeclaration, TSEnumDeclaration, and TSInterfaceDeclaration)
+- Removed AST_NODE_TYPES.Import. This is a minor breaking change as the node type that used this was removed ages ago.
+* **eslint-plugin:** Default rule options is a breaking change.
+
+
+
+
+
## [3.10.1](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.0...v3.10.1) (2020-08-25)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 23094e083494..9190a32dd518 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -7,74 +7,75 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/all-contri
-
+
diff --git a/docs/getting-started/linting/README.md b/docs/getting-started/linting/README.md
index 6abe587e3a32..63163d9a6fd8 100644
--- a/docs/getting-started/linting/README.md
+++ b/docs/getting-started/linting/README.md
@@ -131,6 +131,7 @@ Using this config is as simple as adding it to the end of your `extends`:
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
++ 'prettier',
+ 'prettier/@typescript-eslint',
],
};
diff --git a/lerna.json b/lerna.json
index 4072d24da13a..0402b8bfb850 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "3.10.1",
+ "version": "4.0.0",
"npmClient": "yarn",
"useWorkspaces": true,
"stream": true
diff --git a/package.json b/package.json
index c130fec5ce10..89e20c04d805 100644
--- a/package.json
+++ b/package.json
@@ -66,38 +66,40 @@
"node": "^10.12.0 || >=12.0.0"
},
"devDependencies": {
- "@commitlint/cli": "^8.3.5",
- "@commitlint/config-conventional": "^8.3.4",
- "@commitlint/config-lerna-scopes": "^8.3.4",
- "@types/glob": "^7.1.1",
+ "@commitlint/cli": "^9.1.2",
+ "@commitlint/config-conventional": "^9.1.2",
+ "@commitlint/config-lerna-scopes": "^9.1.2",
+ "@types/debug": "^4.1.5",
+ "@types/glob": "^7.1.3",
+ "@types/jest": "^26.0.10",
"@types/jest-specific-snapshot": "^0.5.4",
- "@types/jest": "^25.2.1",
- "@types/node": "^13.13.5",
- "@types/prettier": "^2.0.0",
+ "@types/lodash": "^4.14.149",
+ "@types/node": "^14.6.2",
+ "@types/prettier": "^2.1.0",
"@types/rimraf": "^3.0.0",
- "all-contributors-cli": "^6.14.2",
- "cspell": "^4.0.61",
- "cz-conventional-changelog": "^3.2.0",
- "downlevel-dts": "^0.4.0",
- "eslint": "^7.2.0",
- "eslint-plugin-eslint-comments": "^3.1.2",
- "eslint-plugin-eslint-plugin": "^2.2.1",
- "eslint-plugin-import": "^2.20.2",
- "eslint-plugin-jest": "^23.10.0",
+ "all-contributors-cli": "^6.17.2",
+ "cspell": "^4.1.0",
+ "cz-conventional-changelog": "^3.3.0",
+ "downlevel-dts": "^0.6.0",
+ "eslint": "^7.7.0",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-eslint-plugin": "^2.3.0",
+ "eslint-plugin-import": "^2.22.0",
+ "eslint-plugin-jest": "^23.20.0",
"glob": "^7.1.6",
"husky": "^4.2.5",
"isomorphic-fetch": "^2.2.1",
- "jest": "^25.5.4",
- "jest-specific-snapshot": "^3.0.0",
- "lerna": "^3.20.2",
- "lint-staged": "^10.2.2",
+ "jest": "^26.4.2",
+ "jest-specific-snapshot": "^4.0.0",
+ "lerna": "^3.22.1",
+ "lint-staged": "^10.2.13",
"make-dir": "^3.1.0",
- "markdownlint-cli": "^0.23.0",
- "prettier": "^2.0.5",
+ "markdownlint-cli": "^0.23.2",
+ "prettier": "^2.1.1",
"rimraf": "^3.0.2",
- "ts-jest": "^25.5.1",
- "ts-node": "^8.10.1",
- "tslint": "^6.1.2",
+ "ts-jest": "^26.3.0",
+ "ts-node": "^9.0.0",
+ "tslint": "^6.1.3",
"typescript": ">=3.3.1 <4.1.0"
},
"resolutions": {
diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md
index e53d3d725107..bbc69802b67f 100644
--- a/packages/eslint-plugin-internal/CHANGELOG.md
+++ b/packages/eslint-plugin-internal/CHANGELOG.md
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [4.0.0](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.1...v4.0.0) (2020-08-31)
+
+
+### Features
+
+* support ESTree optional chaining representation ([#2308](https://github.com/typescript-eslint/typescript-eslint/issues/2308)) ([e9d2ab6](https://github.com/typescript-eslint/typescript-eslint/commit/e9d2ab638b6767700b52797e74b814ea059beaae))
+
+
+
+
+
## [3.10.1](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.0...v3.10.1) (2020-08-25)
**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal
diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json
index 1adff2164ed8..9f8bee817c18 100644
--- a/packages/eslint-plugin-internal/package.json
+++ b/packages/eslint-plugin-internal/package.json
@@ -1,6 +1,6 @@
{
"name": "@typescript-eslint/eslint-plugin-internal",
- "version": "3.10.1",
+ "version": "4.0.0",
"private": true,
"main": "dist/index.js",
"scripts": {
@@ -13,7 +13,8 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
- "@typescript-eslint/experimental-utils": "3.10.1",
+ "@types/prettier": "*",
+ "@typescript-eslint/experimental-utils": "4.0.0",
"prettier": "*"
}
}
diff --git a/packages/eslint-plugin-internal/src/rules/no-poorly-typed-ts-props.ts b/packages/eslint-plugin-internal/src/rules/no-poorly-typed-ts-props.ts
index e112b8205b20..1fa447080ba7 100644
--- a/packages/eslint-plugin-internal/src/rules/no-poorly-typed-ts-props.ts
+++ b/packages/eslint-plugin-internal/src/rules/no-poorly-typed-ts-props.ts
@@ -59,10 +59,8 @@ export default createRule({
const checker = program.getTypeChecker();
return {
- ':matches(MemberExpression, OptionalMemberExpression)[computed = false]'(
- node:
- | TSESTree.MemberExpressionNonComputedName
- | TSESTree.OptionalMemberExpressionNonComputedName,
+ 'MemberExpression[computed = false]'(
+ node: TSESTree.MemberExpressionNonComputedName,
): void {
for (const banned of BANNED_PROPERTIES) {
if (node.property.name !== banned.property) {
diff --git a/packages/eslint-plugin-internal/tests/rules/no-poorly-typed-ts-props.test.ts b/packages/eslint-plugin-internal/tests/rules/no-poorly-typed-ts-props.test.ts
index 63b1f8d5726c..8fe109ee3d13 100644
--- a/packages/eslint-plugin-internal/tests/rules/no-poorly-typed-ts-props.test.ts
+++ b/packages/eslint-plugin-internal/tests/rules/no-poorly-typed-ts-props.test.ts
@@ -90,5 +90,37 @@ thing.getSymbol();
},
],
},
+ {
+ code: `
+import ts from 'typescript';
+declare const thing: ts.Type;
+thing?.symbol;
+ `.trimRight(),
+ errors: [
+ {
+ messageId: 'doNotUseWithFixer',
+ data: {
+ type: 'Type',
+ property: 'symbol',
+ fixWith: 'getSymbol()',
+ },
+ line: 4,
+ suggestions: [
+ {
+ messageId: 'suggestedFix',
+ data: {
+ type: 'Type',
+ fixWith: 'getSymbol()',
+ },
+ output: `
+import ts from 'typescript';
+declare const thing: ts.Type;
+thing?.getSymbol();
+ `.trimRight(),
+ },
+ ],
+ },
+ ],
+ },
],
});
diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md
index 0d598dd62fc4..f6de03fd1795 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.
+# [4.0.0](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.1...v4.0.0) (2020-08-31)
+
+**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint
+
+
+
+
+
## [3.10.1](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.0...v3.10.1) (2020-08-25)
**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 f148419c3b06..39f19cf9e319 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": "3.10.1",
+ "version": "4.0.0",
"main": "dist/index.js",
"typings": "src/index.ts",
"description": "TSLint wrapper plugin for ESLint",
@@ -38,7 +38,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
- "@typescript-eslint/experimental-utils": "3.10.1",
+ "@typescript-eslint/experimental-utils": "4.0.0",
"lodash": "^4.17.15"
},
"peerDependencies": {
@@ -47,7 +47,7 @@
"typescript": "*"
},
"devDependencies": {
- "@types/lodash": "^4.14.149",
- "@typescript-eslint/parser": "3.10.1"
+ "@types/lodash": "*",
+ "@typescript-eslint/parser": "4.0.0"
}
}
diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md
index e5510d75c4e6..b2de24aff30b 100644
--- a/packages/eslint-plugin/CHANGELOG.md
+++ b/packages/eslint-plugin/CHANGELOG.md
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [4.0.0](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.1...v4.0.0) (2020-08-31)
+
+
+### Bug Fixes
+
+* **eslint-plugin:** [no-shadow] fix false-positive on enum declaration ([#2374](https://github.com/typescript-eslint/typescript-eslint/issues/2374)) ([9de669f](https://github.com/typescript-eslint/typescript-eslint/commit/9de669f339fef62a98f745dc08b833aa5c632e62))
+* **eslint-plugin:** [no-unused-vars] handle TSCallSignature ([#2336](https://github.com/typescript-eslint/typescript-eslint/issues/2336)) ([c70f54f](https://github.com/typescript-eslint/typescript-eslint/commit/c70f54fd3a46a12060ae3aec0faae872c431dd88))
+* correct decorator traversal for AssignmentPattern ([#2375](https://github.com/typescript-eslint/typescript-eslint/issues/2375)) ([d738fa4](https://github.com/typescript-eslint/typescript-eslint/commit/d738fa4eff0a5c4cfc9b30b1c0502f8d1e78d7b6))
+* **scope-manager:** correct analysis of abstract class properties ([#2420](https://github.com/typescript-eslint/typescript-eslint/issues/2420)) ([cd84549](https://github.com/typescript-eslint/typescript-eslint/commit/cd84549beba3cf471d75cfd9ba26f80366842ed5))
+* **typescript-estree:** correct ChainExpression interaction with parentheses and non-nulls ([#2380](https://github.com/typescript-eslint/typescript-eslint/issues/2380)) ([762bc99](https://github.com/typescript-eslint/typescript-eslint/commit/762bc99584ede4d0b8099a743991e957aec86aa8))
+
+
+### Features
+
+* consume new scope analysis package ([#2039](https://github.com/typescript-eslint/typescript-eslint/issues/2039)) ([3be125d](https://github.com/typescript-eslint/typescript-eslint/commit/3be125d9bdbee1984ac6037874edf619213bd3d0))
+* support ESTree optional chaining representation ([#2308](https://github.com/typescript-eslint/typescript-eslint/issues/2308)) ([e9d2ab6](https://github.com/typescript-eslint/typescript-eslint/commit/e9d2ab638b6767700b52797e74b814ea059beaae))
+* **eslint-plugin:** [ban-ts-comment] change default for `ts-expect-error` to `allow-with-description` ([#2351](https://github.com/typescript-eslint/typescript-eslint/issues/2351)) ([a3f163a](https://github.com/typescript-eslint/typescript-eslint/commit/a3f163abc03f0fefc6dca1f205b728a4425209e4)), closes [#2146](https://github.com/typescript-eslint/typescript-eslint/issues/2146)
+* **eslint-plugin:** [no-unnecessary-condition][strict-boolean-expressions] add option to make the rules error on files without `strictNullChecks` turned on ([#2345](https://github.com/typescript-eslint/typescript-eslint/issues/2345)) ([9273441](https://github.com/typescript-eslint/typescript-eslint/commit/9273441f7592b52620e10432cb2dd4dc5c3b4db1))
+* **eslint-plugin:** [typedef] remove all defaults ([#2352](https://github.com/typescript-eslint/typescript-eslint/issues/2352)) ([a9cd6fb](https://github.com/typescript-eslint/typescript-eslint/commit/a9cd6fb893074e4f2ca9ad3497eaddfacb3cfd25))
+* **eslint-plugin:** add `consistent-type-imports` rule ([#2367](https://github.com/typescript-eslint/typescript-eslint/issues/2367)) ([58b1c2d](https://github.com/typescript-eslint/typescript-eslint/commit/58b1c2d463f34895798b9a61340e49ffc3ec4f1a))
+
+
+### BREAKING CHANGES
+
+* - Removed decorators property from several Nodes that could never semantically have them (FunctionDeclaration, TSEnumDeclaration, and TSInterfaceDeclaration)
+- Removed AST_NODE_TYPES.Import. This is a minor breaking change as the node type that used this was removed ages ago.
+* **eslint-plugin:** Default rule options is a breaking change.
+
+
+
+
+
## [3.10.1](https://github.com/typescript-eslint/typescript-eslint/compare/v3.10.0...v3.10.1) (2020-08-25)
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 031db6937944..fd77a7dcebab 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -106,6 +106,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Ensures that literals on classes are exposed in a consistent style | | :wrench: | |
| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | | | |
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | |
+| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforces consistent usage of type imports | | :wrench: | |
| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | |
| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | |
| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | :heavy_check_mark: | | |
@@ -145,7 +146,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: |
-| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | |
@@ -185,34 +185,36 @@ In these cases, we create what we call an extension rule; a rule within our plug
**Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information
-| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: |
-| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------ | -------- | ----------------- |
-| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | |
-| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | |
-| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
-| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: |
-| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
-| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | |
-| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | |
-| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | |
-| [`@typescript-eslint/lines-between-class-members`](./docs/rules/lines-between-class-members.md) | Require or disallow an empty line between class members | | :wrench: | |
-| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
-| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | |
-| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | |
-| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | |
-| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | :heavy_check_mark: | :wrench: | |
-| [`@typescript-eslint/no-invalid-this`](./docs/rules/no-invalid-this.md) | disallow `this` keywords outside of classes or class-like objects | | | |
-| [`@typescript-eslint/no-loss-of-precision`](./docs/rules/no-loss-of-precision.md) | Disallow literal numbers that lose precision | | | |
-| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | |
-| [`@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-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | | | |
-| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | |
-| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | |
-| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: |
-| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | :wrench: | :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) | Enforces consistent spacing before function parenthesis | | :wrench: | |
+| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: |
+| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- |
+| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | |
+| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | |
+| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
+| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: |
+| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
+| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | |
+| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | |
+| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | |
+| [`@typescript-eslint/lines-between-class-members`](./docs/rules/lines-between-class-members.md) | Require or disallow an empty line between class members | | :wrench: | |
+| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
+| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | |
+| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | |
+| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | |
+| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | :heavy_check_mark: | :wrench: | |
+| [`@typescript-eslint/no-invalid-this`](./docs/rules/no-invalid-this.md) | disallow `this` keywords outside of classes or class-like objects | | | |
+| [`@typescript-eslint/no-loss-of-precision`](./docs/rules/no-loss-of-precision.md) | Disallow literal numbers that lose precision | | | |
+| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | |
+| [`@typescript-eslint/no-redeclare`](./docs/rules/no-redeclare.md) | Disallow variable redeclaration | | | |
+| [`@typescript-eslint/no-shadow`](./docs/rules/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | | | |
+| [`@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-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | | | |
+| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | |
+| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | |
+| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: |
+| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | :wrench: | :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) | Enforces consistent spacing before function parenthesis | | :wrench: | |
diff --git a/packages/eslint-plugin/docs/rules/ban-ts-comment.md b/packages/eslint-plugin/docs/rules/ban-ts-comment.md
index 60e6508d3bd3..2509793ebd37 100644
--- a/packages/eslint-plugin/docs/rules/ban-ts-comment.md
+++ b/packages/eslint-plugin/docs/rules/ban-ts-comment.md
@@ -29,7 +29,7 @@ interface Options {
}
const defaultOptions: Options = {
- 'ts-expect-error': true,
+ 'ts-expect-error': 'allow-with-description',
'ts-ignore': true,
'ts-nocheck': true,
'ts-check': false,
diff --git a/packages/eslint-plugin/docs/rules/consistent-type-imports.md b/packages/eslint-plugin/docs/rules/consistent-type-imports.md
new file mode 100644
index 000000000000..3487bf9b5ee1
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/consistent-type-imports.md
@@ -0,0 +1,64 @@
+# Enforces consistent usage of type imports (`consistent-type-imports`)
+
+TypeScript 3.8 added support for type-only imports.
+Type-only imports allow you to specify that an import can only be used in a type location, allowing certain optimizations within compilers.
+
+## Rule Details
+
+This rule aims to standardize the use of type imports style across the codebase.
+
+## Options
+
+```ts
+type Options = {
+ prefer: 'type-imports' | 'no-type-imports';
+ disallowTypeAnnotations: boolean;
+};
+
+const defaultOptions: Options = {
+ prefer: 'type-imports',
+ disallowTypeAnnotations: true,
+};
+```
+
+### `prefer`
+
+This option defines the expected import kind for type-only imports. Valid values for `prefer` are:
+
+- `type-imports` will enforce that you always use `import type Foo from '...'`. It is default.
+- `no-type-imports` will enforce that you always use `import Foo from '...'`.
+
+Examples of **correct** code with `{prefer: 'type-imports'}`, and **incorrect** code with `{prefer: 'no-type-imports'}`.
+
+```ts
+import type { Foo } from 'Foo';
+import type Bar from 'Bar';
+type T = Foo;
+const x: Bar = 1;
+```
+
+Examples of **incorrect** code with `{prefer: 'type-imports'}`, and **correct** code with `{prefer: 'no-type-imports'}`.
+
+```ts
+import { Foo } from 'Foo';
+import Bar from 'Bar';
+type T = Foo;
+const x: Bar = 1;
+```
+
+### `disallowTypeAnnotations`
+
+If `true`, type imports in type annotations (`import()`) is not allowed.
+Default is `true`.
+
+Examples of **incorrect** code with `{disallowTypeAnnotations: true}`.
+
+```ts
+type T = import('Foo').Foo;
+const x: import('Bar') = 1;
+```
+
+## When Not To Use It
+
+- If you are not using TypeScript 3.8 (or greater), then you will not be able to use this rule, as type-only imports are not allowed.
+- If you specifically want to use both import kinds for stylistic reasons, you can disable this rule.
diff --git a/packages/eslint-plugin/docs/rules/no-redeclare.md b/packages/eslint-plugin/docs/rules/no-redeclare.md
new file mode 100644
index 000000000000..a794250d6f12
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-redeclare.md
@@ -0,0 +1,69 @@
+# Disallow variable redeclaration (`no-redeclare`)
+
+## Rule Details
+
+This rule extends the base [`eslint/no-redeclare`](https://eslint.org/docs/rules/no-redeclare) rule.
+It adds support for TypeScript function overloads, and declaration merging.
+
+## How to use
+
+```jsonc
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "no-redeclare": "off",
+ "@typescript-eslint/no-redeclare": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/no-redeclare` options](https://eslint.org/docs/rules/no-redeclare#options).
+This rule adds the following options:
+
+```ts
+interface Options extends BaseNoShadowOptions {
+ ignoreDeclarationMerge?: boolean;
+}
+
+const defaultOptions: Options = {
+ ...baseNoShadowDefaultOptions,
+ ignoreDeclarationMerge: true,
+};
+```
+
+### `ignoreDeclarationMerge`
+
+When set to `true`, the rule will ignore declaration merges between the following sets:
+
+- interface + interface
+- namespace + namespace
+- class + interface
+- class + namespace
+- class + interface + namespace
+- function + namespace
+
+Examples of **correct** code with `{ ignoreDeclarationMerge: true }`:
+
+```ts
+interface A {
+ prop1: 1;
+}
+interface A {
+ prop2: 2;
+}
+
+namespace Foo {
+ export const a = 1;
+}
+namespace Foo {
+ export const b = 2;
+}
+
+class Bar {}
+namespace Bar {}
+
+function Baz() {}
+namespace Baz {}
+```
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-redeclare.md)
diff --git a/packages/eslint-plugin/docs/rules/no-shadow.md b/packages/eslint-plugin/docs/rules/no-shadow.md
new file mode 100644
index 000000000000..d4b5294696aa
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-shadow.md
@@ -0,0 +1,50 @@
+# Disallow variable declarations from shadowing variables declared in the outer scope (`no-shadow`)
+
+## Rule Details
+
+This rule extends the base [`eslint/no-shadow`](https://eslint.org/docs/rules/no-shadow) rule.
+It adds support for TypeScript's `this` parameters, and adds options for TypeScript features.
+
+## How to use
+
+```jsonc
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "no-shadow": "off",
+ "@typescript-eslint/no-shadow": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/no-shadow` options](https://eslint.org/docs/rules/no-shadow#options).
+This rule adds the following options:
+
+```ts
+interface Options extends BaseNoShadowOptions {
+ ignoreTypeValueShadow?: boolean;
+}
+
+const defaultOptions: Options = {
+ ...baseNoShadowDefaultOptions,
+ ignoreTypeValueShadow: true,
+};
+```
+
+### `ignoreTypeValueShadow`
+
+When set to `true`, the rule will ignore when you name a type and a variable with the same name.
+
+Examples of **correct** code with `{ ignoreTypeValueShadow: true }`:
+
+```ts
+type Foo = number;
+const Foo = 1;
+
+interface Bar {
+ prop: number;
+}
+const Bar = 'test';
+```
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-shadow.md)
diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md
index 09e6730c3c5d..bff0f548f50e 100644
--- a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md
+++ b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md
@@ -62,11 +62,23 @@ function bar(arg?: string | null) {
## Options
-Accepts an object with the following options:
+```ts
+type Options = {
+ // if true, the rule will ignore constant loop conditions
+ allowConstantLoopConditions?: boolean;
+ // if true, the rule will not error when running with a tsconfig that has strictNullChecks turned **off**
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
+};
+
+const defaultOptions: Options = {
+ allowConstantLoopConditions: false,
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
+};
+```
-- `allowConstantLoopConditions` (default `false`) - allows constant expressions in loops.
+### `allowConstantLoopConditions`
-Example of correct code for when `allowConstantLoopConditions` is `true`:
+Example of correct code for `{ allowConstantLoopConditions: true }`:
```ts
while (true) {}
@@ -74,6 +86,16 @@ for (; true; ) {}
do {} while (true);
```
+### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing`
+
+If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.
+
+Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule useless.
+
+You should be using `strictNullChecks` to ensure complete type-safety in your codebase.
+
+If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option.
+
## 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-vars.md b/packages/eslint-plugin/docs/rules/no-unused-vars.md
index 44cfae690f8e..5eaf167a379a 100644
--- a/packages/eslint-plugin/docs/rules/no-unused-vars.md
+++ b/packages/eslint-plugin/docs/rules/no-unused-vars.md
@@ -1,7 +1,5 @@
# Disallow unused variables (`no-unused-vars`)
-## PLEASE READ THIS ISSUE BEFORE USING THIS RULE [#1856](https://github.com/typescript-eslint/typescript-eslint/issues/1856)
-
## Rule Details
This rule extends the base [`eslint/no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) rule.
diff --git a/packages/eslint-plugin/docs/rules/no-use-before-define.md b/packages/eslint-plugin/docs/rules/no-use-before-define.md
index 569b8130386d..dfdab4d60f70 100644
--- a/packages/eslint-plugin/docs/rules/no-use-before-define.md
+++ b/packages/eslint-plugin/docs/rules/no-use-before-define.md
@@ -23,42 +23,34 @@ See [`eslint/no-use-before-define` options](https://eslint.org/docs/rules/no-use
This rule adds the following options:
```ts
-interface Options extends BaseNoMagicNumbersOptions {
+interface Options extends BaseNoUseBeforeDefineOptions {
enums?: boolean;
typedefs?: boolean;
+ ignoreTypeReferences?: boolean;
}
const defaultOptions: Options = {
- ...baseNoMagicNumbersDefaultOptions,
+ ...baseNoUseBeforeDefineDefaultOptions,
enums: true,
typedefs: true,
+ ignoreTypeReferences: true,
};
```
### `enums`
-The flag which shows whether or not this rule checks enum declarations of upper scopes.
If this is `true`, this rule warns every reference to a enum before the enum declaration.
-Otherwise, ignores those references.
+If this is `false`, this rule will ignore references to enums, when the reference is in a child scope.
-Examples of **incorrect** code for the `{ "enums": true }` option:
+Examples of **incorrect** code for the `{ "enums": false }` option:
```ts
-/*eslint no-use-before-define: ["error", { "enums": true }]*/
+/*eslint no-use-before-define: ["error", { "enums": false }]*/
-function foo() {
- return Foo.FOO;
-}
-
-class Test {
- foo() {
- return Foo.FOO;
- }
-}
+const x = Foo.FOO;
enum Foo {
FOO,
- BAR,
}
```
@@ -78,10 +70,8 @@ enum Foo {
### `typedefs`
-The flag which shows whether or not this rule checks type declarations.
If this is `true`, this rule warns every reference to a type before the type declaration.
-Otherwise, ignores those references.
-Type declarations are hoisted, so it's safe.
+If this is `false`, this rule will ignore references to types.
Examples of **correct** code for the `{ "typedefs": false }` option:
@@ -92,4 +82,25 @@ let myVar: StringOrNumber;
type StringOrNumber = string | number;
```
-Copied from [the original ESLint rule docs](https://github.com/eslint/eslint/blob/a113cd3/docs/rules/no-use-before-define.md)
+### `ignoreTypeReferences`
+
+If this is `true`, this rule ignores all type references, such as in type annotations and assertions.
+If this is `false`, this will will check all type references.
+
+Examples of **correct** code for the `{ "ignoreTypeReferences": true }` option:
+
+```ts
+/*eslint no-use-before-define: ["error", { "ignoreTypeReferences": true }]*/
+
+let var1: StringOrNumber;
+type StringOrNumber = string | number;
+
+let var2: Enum;
+enum Enum {}
+```
+
+### Other Options
+
+See [`eslint/no-use-before-define` options](https://eslint.org/docs/rules/no-use-before-define#options).
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-use-before-define.md)
diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md
index 07284154a311..243d54fda13a 100644
--- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md
+++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md
@@ -79,41 +79,81 @@ const foo = (arg: any) => (Boolean(arg) ? 1 : 0);
## Options
-Options may be provided as an object with:
-
-- `allowString` (`true` by default) -
- Allows `string` in a boolean context.
- This is safe because strings have only one falsy value (`""`).
- Set this to `false` if you prefer the explicit `str != ""` or `str.length > 0` style.
-
-- `allowNumber` (`true` by default) -
- Allows `number` in a boolean context.
- This is safe because numbers have only two falsy values (`0` and `NaN`).
- Set this to `false` if you prefer the explicit `num != 0` and `!Number.isNaN(num)` style.
-
-- `allowNullableObject` (`true` by default) -
- Allows `object | function | symbol | null | undefined` in a boolean context.
- This is safe because objects, functions and symbols don't have falsy values.
- Set this to `false` if you prefer the explicit `obj != null` style.
-
-- `allowNullableBoolean` (`false` by default) -
- Allows `boolean | null | undefined` in a boolean context.
- This is unsafe because nullable booleans can be either `false` or nullish.
- Set this to `false` if you want to enforce explicit `bool ?? false` or `bool ?? true` style.
- Set this to `true` if you don't mind implicitly treating false the same as a nullish value.
-
-- `allowNullableString` (`false` by default) -
- Allows `string | null | undefined` in a boolean context.
- This is unsafe because nullable strings can be either an empty string or nullish.
- Set this to `true` if you don't mind implicitly treating an empty string the same as a nullish value.
-
-- `allowNullableNumber` (`false` by default) -
- Allows `number | null | undefined` in a boolean context.
- This is unsafe because nullable numbers can be either a falsy number or nullish.
- Set this to `true` if you don't mind implicitly treating zero or NaN the same as a nullish value.
-
-- `allowAny` (`false` by default) -
- Allows `any` in a boolean context.
+```ts
+type Options = {
+ allowString?: boolean;
+ allowNumber?: boolean;
+ allowNullableObject?: boolean;
+ allowNullableBoolean?: boolean;
+ allowNullableString?: boolean;
+ allowNullableNumber?: boolean;
+ allowAny?: boolean;
+};
+
+const defaultOptions: Options = {
+ allowString: true,
+ allowNumber: true,
+ allowNullableObject: true,
+ allowNullableBoolean: false,
+ allowNullableString: false,
+ allowNullableNumber: false,
+ allowAny: false,
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
+};
+```
+
+### `allowString`
+
+Allows `string` in a boolean context.
+This is safe because strings have only one falsy value (`""`).
+Set this to `false` if you prefer the explicit `str != ""` or `str.length > 0` style.
+
+### `allowNumber`
+
+Allows `number` in a boolean context.
+This is safe because numbers have only two falsy values (`0` and `NaN`).
+Set this to `false` if you prefer the explicit `num != 0` and `!Number.isNaN(num)` style.
+
+### `allowNullableObject`
+
+Allows `object | function | symbol | null | undefined` in a boolean context.
+This is safe because objects, functions and symbols don't have falsy values.
+Set this to `false` if you prefer the explicit `obj != null` style.
+
+### `allowNullableBoolean`
+
+Allows `boolean | null | undefined` in a boolean context.
+This is unsafe because nullable booleans can be either `false` or nullish.
+Set this to `false` if you want to enforce explicit `bool ?? false` or `bool ?? true` style.
+Set this to `true` if you don't mind implicitly treating false the same as a nullish value.
+
+### `allowNullableString`
+
+Allows `string | null | undefined` in a boolean context.
+This is unsafe because nullable strings can be either an empty string or nullish.
+Set this to `true` if you don't mind implicitly treating an empty string the same as a nullish value.
+
+### `allowNullableNumber`
+
+Allows `number | null | undefined` in a boolean context.
+This is unsafe because nullable numbers can be either a falsy number or nullish.
+Set this to `true` if you don't mind implicitly treating zero or NaN the same as a nullish value.
+
+### `allowAny`
+
+Allows `any` in a boolean context.
+This is unsafe for obvious reasons.
+Set this to `true` at your own risk.
+
+### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing`
+
+If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.
+
+Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule a lot less useful.
+
+You should be using `strictNullChecks` to ensure complete type-safety in your codebase.
+
+If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option.
## Related To
diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md
index 594f7b65e7da..6c96fa0317da 100644
--- a/packages/eslint-plugin/docs/rules/typedef.md
+++ b/packages/eslint-plugin/docs/rules/typedef.md
@@ -16,29 +16,43 @@ class ContainsText {
}
```
-> Note: requiring type annotations unnecessarily can be cumbersome to maintain and generally reduces code readability.
-> TypeScript is often better at inferring types than easily written type annotations would allow.
-> Instead of enabling `typedef`, it is generally recommended to use the `--noImplicitAny` and/or `--strictPropertyInitialization` compiler options to enforce type annotations only when useful.
+**_Note:_** requiring type annotations unnecessarily can be cumbersome to maintain and generally reduces code readability.
+TypeScript is often better at inferring types than easily written type annotations would allow.
+
+**Instead of enabling `typedef`, it is generally recommended to use the `--noImplicitAny` and `--strictPropertyInitialization` compiler options to enforce type annotations only when useful.**
## Rule Details
This rule can enforce type annotations in locations regardless of whether they're required.
This is typically used to maintain consistency for element types that sometimes require them.
-> To enforce type definitions existing on call signatures as per TSLint's `arrow-call-signature` and `call-signature` options, use `explicit-function-return-type`.
+> To enforce type definitions existing on call signatures as per TSLint's `arrow-call-signature` and `call-signature` options, use `explicit-function-return-type`, or `explicit-module-boundary-types`.
## Options
-This rule has an object option that may receive any of the following as booleans:
+```ts
+type Options = {
+ arrayDestructuring?: boolean;
+ arrowParameter?: boolean;
+ memberVariableDeclaration?: boolean;
+ objectDestructuring?: boolean;
+ parameter?: boolean;
+ propertyDeclaration?: boolean;
+ variableDeclaration?: boolean;
+ variableDeclarationIgnoreFunction?: boolean;
+};
-- `"arrayDestructuring"`
-- `"arrowParameter"`: `true` by default
-- `"memberVariableDeclaration"`: `true` by default
-- `"objectDestructuring"`
-- `"parameter"`: `true` by default
-- `"propertyDeclaration"`: `true` by default
-- `"variableDeclaration"`,
-- `"variableDeclarationIgnoreFunction"`
+const defaultOptions: Options = {
+ arrayDestructuring: false,
+ arrowParameter: false,
+ memberVariableDeclaration: false,
+ objectDestructuring: false,
+ parameter: false,
+ propertyDeclaration: false,
+ variableDeclaration: false,
+ variableDeclarationIgnoreFunction: false,
+};
+```
For example, with the following configuration:
@@ -48,7 +62,7 @@ For example, with the following configuration:
"@typescript-eslint/typedef": [
"error",
{
- "arrowParameter": false,
+ "arrowParameter": true,
"variableDeclaration": true
}
]
@@ -56,9 +70,8 @@ For example, with the following configuration:
}
```
-- Type annotations on arrow function parameters are not required
+- Type annotations on arrow function parameters are required
- Type annotations on variables are required
-- Options otherwise adhere to the defaults
### `arrayDestructuring`
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
index 079fefb26685..0a2402cd3ab5 100644
--- a/packages/eslint-plugin/package.json
+++ b/packages/eslint-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@typescript-eslint/eslint-plugin",
- "version": "3.10.1",
+ "version": "4.0.0",
"description": "TypeScript plugin for ESLint",
"keywords": [
"eslint",
@@ -42,7 +42,8 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
- "@typescript-eslint/experimental-utils": "3.10.1",
+ "@typescript-eslint/experimental-utils": "4.0.0",
+ "@typescript-eslint/scope-manager": "4.0.0",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
@@ -50,7 +51,9 @@
"tsutils": "^3.17.1"
},
"devDependencies": {
- "@types/marked": "^0.7.1",
+ "@types/debug": "*",
+ "@types/marked": "^1.1.0",
+ "@types/prettier": "*",
"chalk": "^4.0.0",
"marked": "^1.0.0",
"prettier": "*",
diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts
index c0105c354c2d..c3edea830e83 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -18,6 +18,7 @@ export = {
'@typescript-eslint/comma-spacing': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
+ '@typescript-eslint/consistent-type-imports': 'error',
'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error',
'dot-notation': 'off',
@@ -74,7 +75,11 @@ export = {
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-parameter-properties': 'error',
+ 'no-redeclare': 'off',
+ '@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-require-imports': 'error',
+ 'no-shadow': 'off',
+ '@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-throw-literal': 'error',
'@typescript-eslint/no-type-alias': 'error',
@@ -91,7 +96,6 @@ export = {
'@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',
diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts
index 3eccf04ced20..f8c688415987 100644
--- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts
+++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts
@@ -11,16 +11,6 @@ interface Options {
export const defaultMinimumDescriptionLength = 3;
-const defaultOptions: [Options] = [
- {
- 'ts-expect-error': true,
- 'ts-ignore': true,
- 'ts-nocheck': true,
- 'ts-check': false,
- minimumDescriptionLength: defaultMinimumDescriptionLength,
- },
-];
-
type MessageIds =
| 'tsDirectiveComment'
| 'tsDirectiveCommentRequiresDescription';
@@ -98,7 +88,15 @@ export default util.createRule<[Options], MessageIds>({
},
],
},
- defaultOptions,
+ defaultOptions: [
+ {
+ 'ts-expect-error': 'allow-with-description',
+ 'ts-ignore': true,
+ 'ts-nocheck': true,
+ 'ts-check': false,
+ minimumDescriptionLength: defaultMinimumDescriptionLength,
+ },
+ ],
create(context, [options]) {
const tsCommentRegExp = /^\/*\s*@ts-(expect-error|ignore|check|nocheck)(.*)/;
const sourceCode = context.getSourceCode();
diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts
index cc96778cbe06..adc88dc965ae 100644
--- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts
+++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts
@@ -139,7 +139,6 @@ 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/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts
new file mode 100644
index 000000000000..830c7b6669a6
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts
@@ -0,0 +1,570 @@
+import {
+ TSESLint,
+ TSESTree,
+ AST_TOKEN_TYPES,
+ AST_NODE_TYPES,
+} from '@typescript-eslint/experimental-utils';
+import * as util from '../util';
+
+type Prefer = 'type-imports' | 'no-type-imports';
+
+type Options = [
+ {
+ prefer?: Prefer;
+ disallowTypeAnnotations?: boolean;
+ },
+];
+
+interface SourceImports {
+ source: string;
+ reportValueImports: ReportValueImport[];
+ // ImportDeclaration for type-only import only with named imports.
+ typeOnlyNamedImport: TSESTree.ImportDeclaration | null;
+}
+interface ReportValueImport {
+ node: TSESTree.ImportDeclaration;
+ typeSpecifiers: TSESTree.ImportClause[]; // It has at least one element.
+ valueSpecifiers: TSESTree.ImportClause[];
+ unusedSpecifiers: TSESTree.ImportClause[];
+}
+
+function isImportToken(
+ token: TSESTree.Token | TSESTree.Comment,
+): token is TSESTree.KeywordToken & { value: 'import' } {
+ return token.type === AST_TOKEN_TYPES.Keyword && token.value === 'import';
+}
+
+function isTypeToken(
+ token: TSESTree.Token | TSESTree.Comment,
+): token is TSESTree.IdentifierToken & { value: 'type' } {
+ return token.type === AST_TOKEN_TYPES.Identifier && token.value === 'type';
+}
+type MessageIds =
+ | 'typeOverValue'
+ | 'someImportsAreOnlyTypes'
+ | 'aImportIsOnlyTypes'
+ | 'valueOverType'
+ | 'noImportTypeAnnotations';
+export default util.createRule({
+ name: 'consistent-type-imports',
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'Enforces consistent usage of type imports',
+ category: 'Stylistic Issues',
+ recommended: false,
+ },
+ messages: {
+ typeOverValue:
+ 'All imports in the declaration are only used as types. Use `import type`',
+ someImportsAreOnlyTypes: 'Imports {{typeImports}} are only used as types',
+ aImportIsOnlyTypes: 'Import {{typeImports}} is only used as types',
+ valueOverType: 'Use an `import` instead of an `import type`.',
+ noImportTypeAnnotations: '`import()` type annotations are forbidden.',
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ prefer: {
+ enum: ['type-imports', 'no-type-imports'],
+ },
+ disallowTypeAnnotations: {
+ type: 'boolean',
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ fixable: 'code',
+ },
+
+ defaultOptions: [
+ {
+ prefer: 'type-imports',
+ disallowTypeAnnotations: true,
+ },
+ ],
+
+ create(context, [option]) {
+ const prefer = option.prefer ?? 'type-imports';
+ const disallowTypeAnnotations = option.disallowTypeAnnotations !== false;
+ const sourceCode = context.getSourceCode();
+
+ const sourceImportsMap: { [key: string]: SourceImports } = {};
+
+ return {
+ ...(prefer === 'type-imports'
+ ? {
+ // prefer type imports
+ ImportDeclaration(node: TSESTree.ImportDeclaration): void {
+ const source = node.source.value as string;
+ const sourceImports =
+ sourceImportsMap[source] ??
+ (sourceImportsMap[source] = {
+ source,
+ reportValueImports: [],
+ typeOnlyNamedImport: null,
+ });
+ if (node.importKind === 'type') {
+ if (
+ !sourceImports.typeOnlyNamedImport &&
+ node.specifiers.every(
+ specifier =>
+ specifier.type === AST_NODE_TYPES.ImportSpecifier,
+ )
+ ) {
+ sourceImports.typeOnlyNamedImport = node;
+ }
+ return;
+ }
+ // if importKind === 'value'
+ const typeSpecifiers: TSESTree.ImportClause[] = [];
+ const valueSpecifiers: TSESTree.ImportClause[] = [];
+ const unusedSpecifiers: TSESTree.ImportClause[] = [];
+ for (const specifier of node.specifiers) {
+ const [variable] = context.getDeclaredVariables(specifier);
+ if (variable.references.length === 0) {
+ unusedSpecifiers.push(specifier);
+ } else {
+ const onlyHasTypeReferences = variable.references.every(
+ ref => {
+ if (ref.isValueReference) {
+ // `type T = typeof foo` will create a value reference because "foo" must be a value type
+ // however this value reference is safe to use with type-only imports
+ let parent = ref.identifier.parent;
+ while (parent) {
+ if (parent.type === AST_NODE_TYPES.TSTypeQuery) {
+ return true;
+ }
+ // TSTypeQuery must have a TSESTree.EntityName as its child, so we can filter here and break early
+ if (parent.type !== AST_NODE_TYPES.TSQualifiedName) {
+ break;
+ }
+ parent = parent.parent;
+ }
+ return false;
+ }
+
+ return ref.isTypeReference;
+ },
+ );
+ if (onlyHasTypeReferences) {
+ typeSpecifiers.push(specifier);
+ } else {
+ valueSpecifiers.push(specifier);
+ }
+ }
+ }
+
+ if (typeSpecifiers.length) {
+ sourceImports.reportValueImports.push({
+ node,
+ typeSpecifiers,
+ valueSpecifiers,
+ unusedSpecifiers,
+ });
+ }
+ },
+ 'Program:exit'(): void {
+ for (const sourceImports of Object.values(sourceImportsMap)) {
+ if (sourceImports.reportValueImports.length === 0) {
+ continue;
+ }
+ for (const report of sourceImports.reportValueImports) {
+ if (
+ report.valueSpecifiers.length === 0 &&
+ report.unusedSpecifiers.length === 0
+ ) {
+ // import is all type-only, convert the entire import to `import type`
+ context.report({
+ node: report.node,
+ messageId: 'typeOverValue',
+ *fix(fixer) {
+ yield* fixToTypeImport(fixer, report, sourceImports);
+ },
+ });
+ } else {
+ // we have a mixed type/value import, so we need to split them out into multiple exports
+ const typeImportNames: string[] = report.typeSpecifiers.map(
+ specifier => `"${specifier.local.name}"`,
+ );
+ context.report({
+ node: report.node,
+ messageId:
+ typeImportNames.length === 1
+ ? 'aImportIsOnlyTypes'
+ : 'someImportsAreOnlyTypes',
+ data: {
+ typeImports:
+ typeImportNames.length === 1
+ ? typeImportNames[0]
+ : [
+ typeImportNames.slice(0, -1).join(', '),
+ typeImportNames.slice(-1)[0],
+ ].join(' and '),
+ },
+ *fix(fixer) {
+ yield* fixToTypeImport(fixer, report, sourceImports);
+ },
+ });
+ }
+ }
+ }
+ },
+ }
+ : {
+ // prefer no type imports
+ 'ImportDeclaration[importKind = "type"]'(
+ node: TSESTree.ImportDeclaration,
+ ): void {
+ context.report({
+ node,
+ messageId: 'valueOverType',
+ fix(fixer) {
+ return fixToValueImport(fixer, node);
+ },
+ });
+ },
+ }),
+ ...(disallowTypeAnnotations
+ ? {
+ // disallow `import()` type
+ TSImportType(node: TSESTree.TSImportType): void {
+ context.report({
+ node,
+ messageId: 'noImportTypeAnnotations',
+ });
+ },
+ }
+ : {}),
+ };
+
+ function* fixToTypeImport(
+ fixer: TSESLint.RuleFixer,
+ report: ReportValueImport,
+ sourceImports: SourceImports,
+ ): IterableIterator {
+ const { node } = report;
+
+ const defaultSpecifier: TSESTree.ImportDefaultSpecifier | null =
+ node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier
+ ? node.specifiers[0]
+ : null;
+ const namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null =
+ node.specifiers[0].type === AST_NODE_TYPES.ImportNamespaceSpecifier
+ ? node.specifiers[0]
+ : null;
+ const namedSpecifiers: TSESTree.ImportSpecifier[] = node.specifiers.filter(
+ (specifier): specifier is TSESTree.ImportSpecifier =>
+ specifier.type === AST_NODE_TYPES.ImportSpecifier,
+ );
+
+ if (namespaceSpecifier) {
+ // e.g.
+ // import * as types from 'foo'
+ yield* fixToTypeImportByInsertType(fixer, node, false);
+ return;
+ } else if (defaultSpecifier) {
+ if (
+ report.typeSpecifiers.includes(defaultSpecifier) &&
+ namedSpecifiers.length === 0
+ ) {
+ // e.g.
+ // import Type from 'foo'
+ yield* fixToTypeImportByInsertType(fixer, node, true);
+ return;
+ }
+ } else {
+ if (
+ namedSpecifiers.every(specifier =>
+ report.typeSpecifiers.includes(specifier),
+ )
+ ) {
+ // e.g.
+ // import {Type1, Type2} from 'foo'
+ yield* fixToTypeImportByInsertType(fixer, node, false);
+ return;
+ }
+ }
+
+ const typeNamedSpecifiers = namedSpecifiers.filter(specifier =>
+ report.typeSpecifiers.includes(specifier),
+ );
+
+ const fixesNamedSpecifiers = getFixesNamedSpecifiers(
+ typeNamedSpecifiers,
+ namedSpecifiers,
+ );
+ const afterFixes: TSESLint.RuleFix[] = [];
+ if (typeNamedSpecifiers.length) {
+ if (sourceImports.typeOnlyNamedImport) {
+ const closingBraceToken = util.nullThrows(
+ sourceCode.getFirstTokenBetween(
+ sourceCode.getFirstToken(sourceImports.typeOnlyNamedImport)!,
+ sourceImports.typeOnlyNamedImport.source,
+ util.isClosingBraceToken,
+ ),
+ util.NullThrowsReasons.MissingToken(
+ '}',
+ sourceImports.typeOnlyNamedImport.type,
+ ),
+ );
+ let insertText = fixesNamedSpecifiers.typeNamedSpecifiersText;
+ const before = sourceCode.getTokenBefore(closingBraceToken)!;
+ if (!util.isCommaToken(before) && !util.isOpeningBraceToken(before)) {
+ insertText = ',' + insertText;
+ }
+ // import type { Already, Type1, Type2 } from 'foo'
+ // ^^^^^^^^^^^^^ insert
+ const insertTypeNamedSpecifiers = fixer.insertTextBefore(
+ closingBraceToken,
+ insertText,
+ );
+ if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) {
+ yield insertTypeNamedSpecifiers;
+ } else {
+ afterFixes.push(insertTypeNamedSpecifiers);
+ }
+ } else {
+ yield fixer.insertTextBefore(
+ node,
+ `import type {${
+ fixesNamedSpecifiers.typeNamedSpecifiersText
+ }} from ${sourceCode.getText(node.source)};\n`,
+ );
+ }
+ }
+
+ if (
+ defaultSpecifier &&
+ report.typeSpecifiers.includes(defaultSpecifier)
+ ) {
+ if (typeNamedSpecifiers.length === namedSpecifiers.length) {
+ const importToken = util.nullThrows(
+ sourceCode.getFirstToken(node, isImportToken),
+ util.NullThrowsReasons.MissingToken('import', node.type),
+ );
+ // import type Type from 'foo'
+ // ^^^^ insert
+ yield fixer.insertTextAfter(importToken, ' type');
+ } else {
+ yield fixer.insertTextBefore(
+ node,
+ `import type ${sourceCode.getText(
+ defaultSpecifier,
+ )} from ${sourceCode.getText(node.source)};\n`,
+ );
+ // import Type , {...} from 'foo'
+ // ^^^^^^ remove
+ yield fixer.remove(defaultSpecifier);
+ yield fixer.remove(sourceCode.getTokenAfter(defaultSpecifier)!);
+ }
+ }
+
+ yield* fixesNamedSpecifiers.removeTypeNamedSpecifiers;
+
+ yield* afterFixes;
+
+ /**
+ * Returns information for fixing named specifiers.
+ */
+ function getFixesNamedSpecifiers(
+ typeNamedSpecifiers: TSESTree.ImportSpecifier[],
+ allNamedSpecifiers: TSESTree.ImportSpecifier[],
+ ): {
+ typeNamedSpecifiersText: string;
+ removeTypeNamedSpecifiers: TSESLint.RuleFix[];
+ } {
+ const typeNamedSpecifiersTexts: string[] = [];
+ const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = [];
+ if (typeNamedSpecifiers.length === allNamedSpecifiers.length) {
+ // e.g.
+ // import Foo, {Type1, Type2} from 'foo'
+ // import DefType, {Type1, Type2} from 'foo'
+ const openingBraceToken = util.nullThrows(
+ sourceCode.getTokenBefore(
+ typeNamedSpecifiers[0],
+ util.isOpeningBraceToken,
+ ),
+ util.NullThrowsReasons.MissingToken('{', node.type),
+ );
+ const commaToken = util.nullThrows(
+ sourceCode.getTokenBefore(openingBraceToken, util.isCommaToken),
+ util.NullThrowsReasons.MissingToken(',', node.type),
+ );
+ const closingBraceToken = util.nullThrows(
+ sourceCode.getFirstTokenBetween(
+ openingBraceToken,
+ node.source,
+ util.isClosingBraceToken,
+ ),
+ util.NullThrowsReasons.MissingToken('}', node.type),
+ );
+
+ // import DefType, {...} from 'foo'
+ // ^^^^^^^ remove
+ removeTypeNamedSpecifiers.push(
+ fixer.removeRange([
+ commaToken.range[0],
+ closingBraceToken.range[1],
+ ]),
+ );
+
+ typeNamedSpecifiersTexts.push(
+ sourceCode.text.slice(
+ openingBraceToken.range[1],
+ closingBraceToken.range[0],
+ ),
+ );
+ } else {
+ const typeNamedSpecifierGroups: TSESTree.ImportSpecifier[][] = [];
+ let group: TSESTree.ImportSpecifier[] = [];
+ for (const namedSpecifier of allNamedSpecifiers) {
+ if (typeNamedSpecifiers.includes(namedSpecifier)) {
+ group.push(namedSpecifier);
+ } else if (group.length) {
+ typeNamedSpecifierGroups.push(group);
+ group = [];
+ }
+ }
+ if (group.length) {
+ typeNamedSpecifierGroups.push(group);
+ }
+ for (const namedSpecifiers of typeNamedSpecifierGroups) {
+ const { removeRange, textRange } = getNamedSpecifierRanges(
+ namedSpecifiers,
+ allNamedSpecifiers,
+ );
+ removeTypeNamedSpecifiers.push(fixer.removeRange(removeRange));
+
+ typeNamedSpecifiersTexts.push(sourceCode.text.slice(...textRange));
+ }
+ }
+ return {
+ typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','),
+ removeTypeNamedSpecifiers,
+ };
+ }
+
+ /**
+ * Returns ranges for fixing named specifier.
+ */
+ function getNamedSpecifierRanges(
+ namedSpecifierGroup: TSESTree.ImportSpecifier[],
+ allNamedSpecifiers: TSESTree.ImportSpecifier[],
+ ): {
+ textRange: TSESTree.Range;
+ removeRange: TSESTree.Range;
+ } {
+ const first = namedSpecifierGroup[0];
+ const last = namedSpecifierGroup[namedSpecifierGroup.length - 1];
+ const removeRange: TSESTree.Range = [first.range[0], last.range[1]];
+ const textRange: TSESTree.Range = [...removeRange];
+ const before = sourceCode.getTokenBefore(first)!;
+ textRange[0] = before.range[1];
+ if (util.isCommaToken(before)) {
+ removeRange[0] = before.range[0];
+ } else {
+ removeRange[0] = before.range[1];
+ }
+
+ const isFirst = allNamedSpecifiers[0] === first;
+ const isLast =
+ allNamedSpecifiers[allNamedSpecifiers.length - 1] === last;
+ const after = sourceCode.getTokenAfter(last)!;
+ textRange[1] = after.range[0];
+ if (isFirst || isLast) {
+ if (util.isCommaToken(after)) {
+ removeRange[1] = after.range[1];
+ }
+ }
+
+ return {
+ textRange,
+ removeRange,
+ };
+ }
+ }
+
+ function* fixToTypeImportByInsertType(
+ fixer: TSESLint.RuleFixer,
+ node: TSESTree.ImportDeclaration,
+ isDefaultImport: boolean,
+ ): IterableIterator {
+ // import type Foo from 'foo'
+ // ^^^^^ insert
+ const importToken = util.nullThrows(
+ sourceCode.getFirstToken(node, isImportToken),
+ util.NullThrowsReasons.MissingToken('import', node.type),
+ );
+ yield fixer.insertTextAfter(importToken, ' type');
+
+ if (isDefaultImport) {
+ // Has default import
+ const openingBraceToken = sourceCode.getFirstTokenBetween(
+ importToken,
+ node.source,
+ util.isOpeningBraceToken,
+ );
+ if (openingBraceToken) {
+ // Only braces. e.g. import Foo, {} from 'foo'
+ const commaToken = util.nullThrows(
+ sourceCode.getTokenBefore(openingBraceToken, util.isCommaToken),
+ util.NullThrowsReasons.MissingToken(',', node.type),
+ );
+ const closingBraceToken = util.nullThrows(
+ sourceCode.getFirstTokenBetween(
+ openingBraceToken,
+ node.source,
+ util.isClosingBraceToken,
+ ),
+ util.NullThrowsReasons.MissingToken('}', node.type),
+ );
+
+ // import type Foo, {} from 'foo'
+ // ^^ remove
+ yield fixer.removeRange([
+ commaToken.range[0],
+ closingBraceToken.range[1],
+ ]);
+ const specifiersText = sourceCode.text.slice(
+ commaToken.range[1],
+ closingBraceToken.range[1],
+ );
+ if (node.specifiers.length > 1) {
+ // import type Foo from 'foo'
+ // import type {...} from 'foo' // <- insert
+ yield fixer.insertTextAfter(
+ node,
+ `\nimport type${specifiersText} from ${sourceCode.getText(
+ node.source,
+ )};`,
+ );
+ }
+ }
+ }
+ }
+
+ function fixToValueImport(
+ fixer: TSESLint.RuleFixer,
+ node: TSESTree.ImportDeclaration,
+ ): TSESLint.RuleFix {
+ // import type Foo from 'foo'
+ // ^^^^ remove
+ const importToken = util.nullThrows(
+ sourceCode.getFirstToken(node, isImportToken),
+ util.NullThrowsReasons.MissingToken('import', node.type),
+ );
+ const typeToken = util.nullThrows(
+ sourceCode.getFirstTokenBetween(
+ importToken,
+ node.specifiers[0]?.local ?? node.source,
+ isTypeToken,
+ ),
+ util.NullThrowsReasons.MissingToken('type', node.type),
+ );
+ return fixer.remove(typeToken);
+ }
+ },
+});
diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts
index 21a62af1950d..08d72181eeb6 100644
--- a/packages/eslint-plugin/src/rules/func-call-spacing.ts
+++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts
@@ -77,12 +77,9 @@ export default util.createRule({
* @private
*/
function checkSpacing(
- node:
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression
- | TSESTree.NewExpression,
+ node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
- const isOptionalCall = util.isOptionalOptionalCallExpression(node);
+ const isOptionalCall = util.isOptionalCallExpression(node);
const closingParenToken = sourceCode.getLastToken(node)!;
const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken(
@@ -175,7 +172,6 @@ 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 63acccee914d..f909be42d4e8 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -10,6 +10,7 @@ import commaSpacing from './comma-spacing';
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
import consistentTypeAssertions from './consistent-type-assertions';
import consistentTypeDefinitions from './consistent-type-definitions';
+import consistentTypeImports from './consistent-type-imports';
import defaultParamLast from './default-param-last';
import dotNotation from './dot-notation';
import explicitFunctionReturnType from './explicit-function-return-type';
@@ -51,7 +52,9 @@ import noNamespace from './no-namespace';
import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain';
import noNonNullAssertion from './no-non-null-assertion';
import noParameterProperties from './no-parameter-properties';
+import noRedeclare from './no-redeclare';
import noRequireImports from './no-require-imports';
+import noShadow from './no-shadow';
import noThisAlias from './no-this-alias';
import noThrowLiteral from './no-throw-literal';
import noTypeAlias from './no-type-alias';
@@ -114,6 +117,7 @@ export default {
'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual,
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
+ 'consistent-type-imports': consistentTypeImports,
'default-param-last': defaultParamLast,
'dot-notation': dotNotation,
'explicit-function-return-type': explicitFunctionReturnType,
@@ -152,7 +156,9 @@ export default {
'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain,
'no-non-null-assertion': noNonNullAssertion,
'no-parameter-properties': noParameterProperties,
+ 'no-redeclare': noRedeclare,
'no-require-imports': noRequireImports,
+ 'no-shadow': noShadow,
'no-this-alias': noThisAlias,
'no-throw-literal': noThrowLiteral,
'no-type-alias': noTypeAlias,
diff --git a/packages/eslint-plugin/src/rules/no-array-constructor.ts b/packages/eslint-plugin/src/rules/no-array-constructor.ts
index c9465e707996..f9d09439cbb4 100644
--- a/packages/eslint-plugin/src/rules/no-array-constructor.ts
+++ b/packages/eslint-plugin/src/rules/no-array-constructor.ts
@@ -27,17 +27,14 @@ export default util.createRule({
* @param node node to evaluate
*/
function check(
- node:
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression
- | TSESTree.NewExpression,
+ node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
if (
node.arguments.length !== 1 &&
node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === 'Array' &&
!node.typeParameters &&
- !util.isOptionalOptionalCallExpression(node)
+ !util.isOptionalCallExpression(node)
) {
context.report({
node,
@@ -60,7 +57,6 @@ export default util.createRule({
return {
CallExpression: check,
- OptionalCallExpression: check,
NewExpression: check,
};
},
diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts
index 7f5a03a8c39a..246680c60e34 100644
--- a/packages/eslint-plugin/src/rules/no-empty-function.ts
+++ b/packages/eslint-plugin/src/rules/no-empty-function.ts
@@ -129,9 +129,7 @@ export default util.createRule({
): boolean {
if (isAllowedDecoratedFunctions && isBodyEmpty(node)) {
const decorators =
- node.type === AST_NODE_TYPES.FunctionDeclaration
- ? node.decorators
- : node.parent?.type === AST_NODE_TYPES.MethodDefinition
+ node.parent?.type === AST_NODE_TYPES.MethodDefinition
? node.parent.decorators
: undefined;
return !!decorators && !!decorators.length;
diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts
index 817d1232be2b..11ed8582c75b 100644
--- a/packages/eslint-plugin/src/rules/no-empty-interface.ts
+++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts
@@ -1,8 +1,5 @@
import * as util from '../util';
-import {
- AST_NODE_TYPES,
- TSESLint,
-} from '@typescript-eslint/experimental-utils';
+import { TSESLint } from '@typescript-eslint/experimental-utils';
type Options = [
{
@@ -81,12 +78,7 @@ export default util.createRule({
let useAutoFix = true;
if (util.isDefinitionFile(filename)) {
const scope = context.getScope();
- if (
- scope.block.parent &&
- scope.block.parent.type ===
- AST_NODE_TYPES.TSModuleDeclaration &&
- scope.block.parent.declare
- ) {
+ if (scope.type === 'tsModule' && scope.block.declare) {
useAutoFix = false;
}
}
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
index 62b317e30cb3..b0b4921dfaf1 100644
--- a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts
+++ b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts
@@ -32,8 +32,8 @@ export default util.createRule({
return {
'TSNonNullExpression > TSNonNullExpression': checkExtraNonNullAssertion,
- 'OptionalMemberExpression[optional = true] > TSNonNullExpression': checkExtraNonNullAssertion,
- 'OptionalCallExpression[optional = true] > TSNonNullExpression.callee': checkExtraNonNullAssertion,
+ 'MemberExpression[optional = true] > TSNonNullExpression': checkExtraNonNullAssertion,
+ 'CallExpression[optional = true] > TSNonNullExpression.callee': checkExtraNonNullAssertion,
};
},
});
diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts
index 2b84414dfcd7..d6b6538a95d4 100644
--- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts
+++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts
@@ -53,9 +53,12 @@ export default util.createRule({
init: TSESTree.Expression,
callName: string,
): boolean {
+ if (init.type === AST_NODE_TYPES.ChainExpression) {
+ return isFunctionCall(init.expression, callName);
+ }
+
return (
- (init.type === AST_NODE_TYPES.CallExpression ||
- init.type === AST_NODE_TYPES.OptionalCallExpression) &&
+ init.type === AST_NODE_TYPES.CallExpression &&
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 bd74721c7a24..36ad6f676def 100644
--- a/packages/eslint-plugin/src/rules/no-misused-promises.ts
+++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts
@@ -71,7 +71,6 @@ export default util.createRule({
const voidReturnChecks: TSESLint.RuleListener = {
CallExpression: checkArguments,
- OptionalCallExpression: checkArguments,
NewExpression: checkArguments,
};
@@ -94,10 +93,7 @@ export default util.createRule({
}
function checkArguments(
- node:
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression
- | TSESTree.NewExpression,
+ node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const voidParams = voidFunctionParams(checker, tsNode);
diff --git a/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts
index fa14fa661b6d..f5652b257c14 100644
--- a/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts
+++ b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts
@@ -3,18 +3,19 @@ import {
TSESLint,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
-import { version } from 'typescript';
+import * as ts from 'typescript';
import * as semver from 'semver';
import * as util from '../util';
-type MessageIds = 'noNonNullOptionalChain' | 'suggestRemovingNonNull';
-
-const is3dot9 = !semver.satisfies(
- version,
- '< 3.9.0 || < 3.9.1-rc || < 3.9.0-beta',
+const is3dot9 = semver.satisfies(
+ ts.version,
+ `>= 3.9.0 || >= 3.9.1-rc || >= 3.9.0-beta`,
+ {
+ includePrerelease: true,
+ },
);
-export default util.createRule<[], MessageIds>({
+export default util.createRule({
name: 'no-non-null-asserted-optional-chain',
meta: {
type: 'problem',
@@ -34,30 +35,22 @@ export default util.createRule<[], MessageIds>({
},
defaultOptions: [],
create(context) {
- return {
- 'TSNonNullExpression > :matches(OptionalMemberExpression, OptionalCallExpression)'(
- node:
- | TSESTree.OptionalCallExpression
- | TSESTree.OptionalMemberExpression,
- ): void {
- if (is3dot9) {
- // TS3.9 made a breaking change to how non-null works with optional chains.
- // Pre-3.9, `x?.y!.z` means `(x?.y).z` - i.e. it essentially scrubbed the optionality from the chain
- // Post-3.9, `x?.y!.z` means `x?.y!.z` - i.e. it just asserts that the property `y` is non-null, not the result of `x?.y`.
- // This means that for > 3.9, x?.y!.z is valid!
- // NOTE: these cases are still invalid:
- // - x?.y.z!
- // - (x?.y)!.z
- const nnAssertionParent = node.parent?.parent;
- if (
- nnAssertionParent?.type ===
- AST_NODE_TYPES.OptionalMemberExpression ||
- nnAssertionParent?.type === AST_NODE_TYPES.OptionalCallExpression
- ) {
- return;
- }
- }
+ // TS3.9 made a breaking change to how non-null works with optional chains.
+ // Pre-3.9, `x?.y!.z` means `(x?.y).z` - i.e. it essentially scrubbed the optionality from the chain
+ // Post-3.9, `x?.y!.z` means `x?.y!.z` - i.e. it just asserts that the property `y` is non-null, not the result of `x?.y`.
+ // This means that for > 3.9, x?.y!.z is valid!
+ //
+ // NOTE: these cases are still invalid for 3.9:
+ // - x?.y.z!
+ // - (x?.y)!.z
+ const baseSelectors = {
+ // non-nulling a wrapped chain will scrub all nulls introduced by the chain
+ // (x?.y)!
+ // (x?.())!
+ 'TSNonNullExpression > ChainExpression'(
+ node: TSESTree.ChainExpression,
+ ): void {
// selector guarantees this assertion
const parent = node.parent as TSESTree.TSNonNullExpression;
context.report({
@@ -77,6 +70,84 @@ export default util.createRule<[], MessageIds>({
],
});
},
+
+ // non-nulling at the end of a chain will scrub all nulls introduced by the chain
+ // x?.y!
+ // x?.()!
+ 'ChainExpression > TSNonNullExpression'(
+ node: TSESTree.TSNonNullExpression,
+ ): void {
+ context.report({
+ node,
+ messageId: 'noNonNullOptionalChain',
+ // use a suggestion instead of a fixer, because this can obviously break type checks
+ suggest: [
+ {
+ messageId: 'suggestRemovingNonNull',
+ fix(fixer): TSESLint.RuleFix {
+ return fixer.removeRange([node.range[1] - 1, node.range[1]]);
+ },
+ },
+ ],
+ });
+ },
+ };
+
+ if (is3dot9) {
+ return baseSelectors;
+ }
+
+ return {
+ ...baseSelectors,
+ [[
+ // > :not(ChainExpression) because that case is handled by a previous selector
+ 'MemberExpression > TSNonNullExpression.object > :not(ChainExpression)',
+ 'CallExpression > TSNonNullExpression.callee > :not(ChainExpression)',
+ ].join(', ')](child: TSESTree.Node): void {
+ // selector guarantees this assertion
+ const node = child.parent as TSESTree.TSNonNullExpression;
+
+ let current = child;
+ while (current) {
+ switch (current.type) {
+ case AST_NODE_TYPES.MemberExpression:
+ if (current.optional) {
+ // found an optional chain! stop traversing
+ break;
+ }
+
+ current = current.object;
+ continue;
+
+ case AST_NODE_TYPES.CallExpression:
+ if (current.optional) {
+ // found an optional chain! stop traversing
+ break;
+ }
+
+ current = current.callee;
+ continue;
+
+ default:
+ // something that's not a ChainElement, which means this is not an optional chain we want to check
+ return;
+ }
+ }
+
+ context.report({
+ node,
+ messageId: 'noNonNullOptionalChain',
+ // use a suggestion instead of a fixer, because this can obviously break type checks
+ suggest: [
+ {
+ messageId: 'suggestRemovingNonNull',
+ fix(fixer): TSESLint.RuleFix {
+ return fixer.removeRange([node.range[1] - 1, node.range[1]]);
+ },
+ },
+ ],
+ });
+ },
};
},
});
diff --git a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts
index 143b889b17c4..4012693c7554 100644
--- a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts
+++ b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts
@@ -59,60 +59,56 @@ export default util.createRule<[], MessageIds>({
};
}
- if (node.parent) {
- if (
- (node.parent.type === AST_NODE_TYPES.MemberExpression ||
- node.parent.type === AST_NODE_TYPES.OptionalMemberExpression) &&
- node.parent.object === node
- ) {
- if (!node.parent.optional) {
- if (node.parent.computed) {
- // it is x![y]?.z
- suggest.push({
- messageId: 'suggestOptionalChain',
- fix: convertTokenToOptional('?.'),
- });
- } else {
- // it is x!.y?.z
- suggest.push({
- messageId: 'suggestOptionalChain',
- fix: convertTokenToOptional('?'),
- });
- }
+ if (
+ node.parent?.type === AST_NODE_TYPES.MemberExpression &&
+ node.parent.object === node
+ ) {
+ if (!node.parent.optional) {
+ if (node.parent.computed) {
+ // it is x![y]?.z
+ suggest.push({
+ messageId: 'suggestOptionalChain',
+ fix: convertTokenToOptional('?.'),
+ });
} else {
- if (node.parent.computed) {
- // it is x!?.[y].z
- suggest.push({
- messageId: 'suggestOptionalChain',
- fix: removeToken(),
- });
- } else {
- // it is x!?.y.z
- suggest.push({
- messageId: 'suggestOptionalChain',
- fix: removeToken(),
- });
- }
+ // it is x!.y?.z
+ suggest.push({
+ messageId: 'suggestOptionalChain',
+ fix: convertTokenToOptional('?'),
+ });
}
- } else if (
- (node.parent.type === AST_NODE_TYPES.CallExpression ||
- node.parent.type === AST_NODE_TYPES.OptionalCallExpression) &&
- node.parent.callee === node
- ) {
- if (!node.parent.optional) {
- // it is x.y?.z!()
+ } else {
+ if (node.parent.computed) {
+ // it is x!?.[y].z
suggest.push({
messageId: 'suggestOptionalChain',
- fix: convertTokenToOptional('?.'),
+ fix: removeToken(),
});
} else {
- // it is x.y.z!?.()
+ // it is x!?.y.z
suggest.push({
messageId: 'suggestOptionalChain',
fix: removeToken(),
});
}
}
+ } else if (
+ node.parent?.type === AST_NODE_TYPES.CallExpression &&
+ node.parent.callee === node
+ ) {
+ if (!node.parent.optional) {
+ // it is x.y?.z!()
+ suggest.push({
+ messageId: 'suggestOptionalChain',
+ fix: convertTokenToOptional('?.'),
+ });
+ } else {
+ // it is x.y.z!?.()
+ suggest.push({
+ messageId: 'suggestOptionalChain',
+ fix: removeToken(),
+ });
+ }
}
context.report({
diff --git a/packages/eslint-plugin/src/rules/no-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts
new file mode 100644
index 000000000000..d85cb1dc2413
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-redeclare.ts
@@ -0,0 +1,297 @@
+import {
+ TSESTree,
+ TSESLint,
+ AST_NODE_TYPES,
+} from '@typescript-eslint/experimental-utils';
+import * as util from '../util';
+
+type MessageIds = 'redeclared' | 'redeclaredAsBuiltin' | 'redeclaredBySyntax';
+type Options = [
+ {
+ builtinGlobals?: boolean;
+ ignoreDeclarationMerge?: boolean;
+ },
+];
+
+// https://github.com/lodash/lodash/blob/86a852fe763935bb64c12589df5391fd7d3bb14d/escapeRegExp.js
+const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
+const reHasRegExpChar = RegExp(reRegExpChar.source);
+function escapeRegExp(str: string): string {
+ return str && reHasRegExpChar.test(str)
+ ? str.replace(reRegExpChar, '\\$&')
+ : str || '';
+}
+
+function getNameLocationInGlobalDirectiveComment(
+ sourceCode: TSESLint.SourceCode,
+ comment: TSESTree.Comment,
+ name: string,
+): TSESTree.SourceLocation {
+ const namePattern = new RegExp(
+ `[\\s,]${escapeRegExp(name)}(?:$|[\\s,:])`,
+ 'gu',
+ );
+
+ // To ignore the first text "global".
+ namePattern.lastIndex = comment.value.indexOf('global') + 6;
+
+ // Search a given variable name.
+ const match = namePattern.exec(comment.value);
+
+ // Convert the index to loc.
+ const start = sourceCode.getLocFromIndex(
+ comment.range[0] + '/*'.length + (match ? match.index + 1 : 0),
+ );
+ const end = {
+ line: start.line,
+ column: start.column + (match ? name.length : 1),
+ };
+
+ return { start, end };
+}
+
+export default util.createRule({
+ name: 'no-redeclare',
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'Disallow variable redeclaration',
+ category: 'Best Practices',
+ recommended: false,
+ extendsBaseRule: true,
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ builtinGlobals: {
+ type: 'boolean',
+ },
+ ignoreDeclarationMerge: {
+ type: 'boolean',
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ redeclared: "'{{id}}' is already defined.",
+ redeclaredAsBuiltin:
+ "'{{id}}' is already defined as a built-in global variable.",
+ redeclaredBySyntax:
+ "'{{id}}' is already defined by a variable declaration.",
+ },
+ },
+ defaultOptions: [
+ {
+ builtinGlobals: true,
+ ignoreDeclarationMerge: true,
+ },
+ ],
+ create(context, [options]) {
+ const sourceCode = context.getSourceCode();
+
+ const CLASS_DECLARATION_MERGE_NODES = new Set([
+ AST_NODE_TYPES.TSInterfaceDeclaration,
+ AST_NODE_TYPES.TSModuleDeclaration,
+ AST_NODE_TYPES.ClassDeclaration,
+ ]);
+ const FUNCTION_DECLARATION_MERGE_NODES = new Set([
+ AST_NODE_TYPES.TSModuleDeclaration,
+ AST_NODE_TYPES.FunctionDeclaration,
+ ]);
+
+ function* iterateDeclarations(
+ variable: TSESLint.Scope.Variable,
+ ): Generator<
+ {
+ type: 'builtin' | 'syntax' | 'comment';
+ node?: TSESTree.Identifier | TSESTree.Comment;
+ loc?: TSESTree.SourceLocation;
+ },
+ void,
+ unknown
+ > {
+ if (
+ options?.builtinGlobals &&
+ 'eslintImplicitGlobalSetting' in variable &&
+ (variable.eslintImplicitGlobalSetting === 'readonly' ||
+ variable.eslintImplicitGlobalSetting === 'writable')
+ ) {
+ yield { type: 'builtin' };
+ }
+
+ if (
+ 'eslintExplicitGlobalComments' in variable &&
+ variable.eslintExplicitGlobalComments
+ ) {
+ for (const comment of variable.eslintExplicitGlobalComments) {
+ yield {
+ type: 'comment',
+ node: comment,
+ loc: getNameLocationInGlobalDirectiveComment(
+ sourceCode,
+ comment,
+ variable.name,
+ ),
+ };
+ }
+ }
+
+ const identifiers = variable.identifiers
+ .map(id => ({
+ identifier: id,
+ parent: id.parent!,
+ }))
+ // ignore function declarations because TS will treat them as an overload
+ .filter(
+ ({ parent }) => parent.type !== AST_NODE_TYPES.TSDeclareFunction,
+ );
+
+ if (options.ignoreDeclarationMerge && identifiers.length > 1) {
+ if (
+ // interfaces merging
+ identifiers.every(
+ ({ parent }) =>
+ parent.type === AST_NODE_TYPES.TSInterfaceDeclaration,
+ )
+ ) {
+ return;
+ }
+
+ if (
+ // namespace/module merging
+ identifiers.every(
+ ({ parent }) => parent.type === AST_NODE_TYPES.TSModuleDeclaration,
+ )
+ ) {
+ return;
+ }
+
+ if (
+ // class + interface/namespace merging
+ identifiers.every(({ parent }) =>
+ CLASS_DECLARATION_MERGE_NODES.has(parent.type),
+ )
+ ) {
+ const classDecls = identifiers.filter(
+ ({ parent }) => parent.type === AST_NODE_TYPES.ClassDeclaration,
+ );
+ if (classDecls.length === 1) {
+ // safe declaration merging
+ return;
+ }
+
+ // there's more than one class declaration, which needs to be reported
+ for (const { identifier } of classDecls) {
+ yield { type: 'syntax', node: identifier, loc: identifier.loc };
+ }
+ return;
+ }
+
+ if (
+ // class + interface/namespace merging
+ identifiers.every(({ parent }) =>
+ FUNCTION_DECLARATION_MERGE_NODES.has(parent.type),
+ )
+ ) {
+ const functionDecls = identifiers.filter(
+ ({ parent }) => parent.type === AST_NODE_TYPES.FunctionDeclaration,
+ );
+ if (functionDecls.length === 1) {
+ // safe declaration merging
+ return;
+ }
+
+ // there's more than one class declaration, which needs to be reported
+ for (const { identifier } of functionDecls) {
+ yield { type: 'syntax', node: identifier, loc: identifier.loc };
+ }
+ return;
+ }
+ }
+
+ for (const { identifier } of identifiers) {
+ yield { type: 'syntax', node: identifier, loc: identifier.loc };
+ }
+ }
+
+ function findVariablesInScope(scope: TSESLint.Scope.Scope): void {
+ for (const variable of scope.variables) {
+ const [declaration, ...extraDeclarations] = iterateDeclarations(
+ variable,
+ );
+
+ if (extraDeclarations.length === 0) {
+ continue;
+ }
+
+ /*
+ * If the type of a declaration is different from the type of
+ * the first declaration, it shows the location of the first
+ * declaration.
+ */
+ const detailMessageId =
+ declaration.type === 'builtin'
+ ? 'redeclaredAsBuiltin'
+ : 'redeclaredBySyntax';
+ const data = { id: variable.name };
+
+ // Report extra declarations.
+ for (const { type, node, loc } of extraDeclarations) {
+ const messageId =
+ type === declaration.type ? 'redeclared' : detailMessageId;
+
+ if (node) {
+ context.report({ node, loc, messageId, data });
+ } else if (loc) {
+ context.report({ loc, messageId, data });
+ }
+ }
+ }
+ }
+
+ /**
+ * Find variables in the current scope.
+ */
+ function checkForBlock(node: TSESTree.Node): void {
+ const scope = context.getScope();
+
+ /*
+ * In ES5, some node type such as `BlockStatement` doesn't have that scope.
+ * `scope.block` is a different node in such a case.
+ */
+ if (scope.block === node) {
+ findVariablesInScope(scope);
+ }
+ }
+
+ return {
+ Program(): void {
+ const scope = context.getScope();
+
+ findVariablesInScope(scope);
+
+ // Node.js or ES modules has a special scope.
+ if (
+ scope.type === 'global' &&
+ scope.childScopes[0] &&
+ // The special scope's block is the Program node.
+ scope.block === scope.childScopes[0].block
+ ) {
+ findVariablesInScope(scope.childScopes[0]);
+ }
+ },
+
+ FunctionDeclaration: checkForBlock,
+ FunctionExpression: checkForBlock,
+ ArrowFunctionExpression: checkForBlock,
+
+ BlockStatement: checkForBlock,
+ ForStatement: checkForBlock,
+ ForInStatement: checkForBlock,
+ ForOfStatement: checkForBlock,
+ SwitchStatement: checkForBlock,
+ };
+ },
+});
diff --git a/packages/eslint-plugin/src/rules/no-require-imports.ts b/packages/eslint-plugin/src/rules/no-require-imports.ts
index 25f4f35c77be..91d81e0d8c53 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 {
- ':matches(CallExpression, OptionalCallExpression) > Identifier[name="require"]'(
+ 'CallExpression > Identifier[name="require"]'(
node: TSESTree.Identifier,
): void {
context.report({
diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts
new file mode 100644
index 000000000000..9c77ca321fd6
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shadow.ts
@@ -0,0 +1,308 @@
+import {
+ TSESTree,
+ TSESLint,
+ AST_NODE_TYPES,
+} from '@typescript-eslint/experimental-utils';
+import * as util from '../util';
+
+type MessageIds = 'noShadow';
+type Options = [
+ {
+ allow?: string[];
+ builtinGlobals?: boolean;
+ hoist?: 'all' | 'functions' | 'never';
+ ignoreTypeValueShadow?: boolean;
+ },
+];
+
+export default util.createRule({
+ name: 'no-shadow',
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'Disallow variable declarations from shadowing variables declared in the outer scope',
+ category: 'Variables',
+ recommended: false,
+ extendsBaseRule: true,
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ builtinGlobals: {
+ type: 'boolean',
+ },
+ hoist: {
+ enum: ['all', 'functions', 'never'],
+ },
+ allow: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ ignoreTypeValueShadow: {
+ type: 'boolean',
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ noShadow: "'{{name}}' is already declared in the upper scope.",
+ },
+ },
+ defaultOptions: [
+ {
+ allow: [],
+ builtinGlobals: false,
+ hoist: 'functions',
+ ignoreTypeValueShadow: true,
+ },
+ ],
+ create(context, [options]) {
+ /**
+ * Check if variable is a `this` parameter.
+ */
+ function isThisParam(variable: TSESLint.Scope.Variable): boolean {
+ return variable.defs[0].type === 'Parameter' && variable.name === 'this';
+ }
+
+ function isTypeValueShadow(
+ variable: TSESLint.Scope.Variable,
+ shadowed: TSESLint.Scope.Variable,
+ ): boolean {
+ if (options.ignoreTypeValueShadow !== true) {
+ return false;
+ }
+
+ if (
+ !('isValueVariable' in shadowed) ||
+ !('isValueVariable' in variable)
+ ) {
+ // one of them is an eslint global variable
+ return false;
+ }
+
+ return variable.isValueVariable !== shadowed.isValueVariable;
+ }
+
+ /**
+ * Check if variable name is allowed.
+ * @param variable The variable to check.
+ * @returns Whether or not the variable name is allowed.
+ */
+ function isAllowed(variable: TSESLint.Scope.Variable): boolean {
+ return options.allow!.indexOf(variable.name) !== -1;
+ }
+
+ /**
+ * Checks if a variable of the class name in the class scope of ClassDeclaration.
+ *
+ * ClassDeclaration creates two variables of its name into its outer scope and its class scope.
+ * So we should ignore the variable in the class scope.
+ * @param variable The variable to check.
+ * @returns Whether or not the variable of the class name in the class scope of ClassDeclaration.
+ */
+ function isDuplicatedClassNameVariable(
+ variable: TSESLint.Scope.Variable,
+ ): boolean {
+ const block = variable.scope.block;
+
+ return (
+ block.type === AST_NODE_TYPES.ClassDeclaration &&
+ block.id === variable.identifiers[0]
+ );
+ }
+
+ /**
+ * Checks if a variable of the class name in the class scope of ClassDeclaration.
+ *
+ * ClassDeclaration creates two variables of its name into its outer scope and its class scope.
+ * So we should ignore the variable in the class scope.
+ * @param variable The variable to check.
+ * @returns Whether or not the variable of the class name in the class scope of ClassDeclaration.
+ */
+ function isDuplicatedEnumNameVariable(
+ variable: TSESLint.Scope.Variable,
+ ): boolean {
+ const block = variable.scope.block;
+
+ return (
+ block.type === AST_NODE_TYPES.TSEnumDeclaration &&
+ block.id === variable.identifiers[0]
+ );
+ }
+
+ /**
+ * Checks if a variable is inside the initializer of scopeVar.
+ *
+ * To avoid reporting at declarations such as `var a = function a() {};`.
+ * But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`.
+ * @param variable The variable to check.
+ * @param scopeVar The scope variable to look for.
+ * @returns Whether or not the variable is inside initializer of scopeVar.
+ */
+ function isOnInitializer(
+ variable: TSESLint.Scope.Variable,
+ scopeVar: TSESLint.Scope.Variable,
+ ): boolean {
+ const outerScope = scopeVar.scope;
+ const outerDef = scopeVar.defs[0];
+ const outer = outerDef?.parent?.range;
+ const innerScope = variable.scope;
+ const innerDef = variable.defs[0];
+ const inner = innerDef?.name.range;
+
+ return !!(
+ outer &&
+ inner &&
+ outer[0] < inner[0] &&
+ inner[1] < outer[1] &&
+ ((innerDef.type === 'FunctionName' &&
+ innerDef.node.type === AST_NODE_TYPES.FunctionExpression) ||
+ innerDef.node.type === AST_NODE_TYPES.ClassExpression) &&
+ outerScope === innerScope.upper
+ );
+ }
+
+ /**
+ * Get a range of a variable's identifier node.
+ * @param variable The variable to get.
+ * @returns The range of the variable's identifier node.
+ */
+ function getNameRange(
+ variable: TSESLint.Scope.Variable,
+ ): TSESTree.Range | undefined {
+ const def = variable.defs[0];
+ return def?.name.range;
+ }
+
+ /**
+ * Checks if a variable is in TDZ of scopeVar.
+ * @param variable The variable to check.
+ * @param scopeVar The variable of TDZ.
+ * @returns Whether or not the variable is in TDZ of scopeVar.
+ */
+ function isInTdz(
+ variable: TSESLint.Scope.Variable,
+ scopeVar: TSESLint.Scope.Variable,
+ ): boolean {
+ const outerDef = scopeVar.defs[0];
+ const inner = getNameRange(variable);
+ const outer = getNameRange(scopeVar);
+
+ return !!(
+ inner &&
+ outer &&
+ inner[1] < outer[0] &&
+ // Excepts FunctionDeclaration if is {"hoist":"function"}.
+ (options.hoist !== 'functions' ||
+ !outerDef ||
+ outerDef.node.type !== AST_NODE_TYPES.FunctionDeclaration)
+ );
+ }
+
+ /**
+ * Finds the variable by a given name in a given scope and its upper scopes.
+ * @param initScope A scope to start find.
+ * @param name A variable name to find.
+ * @returns A found variable or `null`.
+ */
+ function getVariableByName(
+ initScope: TSESLint.Scope.Scope | null,
+ name: string,
+ ): TSESLint.Scope.Variable | null {
+ let scope = initScope;
+
+ while (scope) {
+ const variable = scope.set.get(name);
+
+ if (variable) {
+ return variable;
+ }
+
+ scope = scope.upper;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks the current context for shadowed variables.
+ * @param {Scope} scope Fixme
+ */
+ function checkForShadows(scope: TSESLint.Scope.Scope): void {
+ const variables = scope.variables;
+
+ for (const variable of variables) {
+ // ignore "arguments"
+ if (variable.identifiers.length === 0) {
+ continue;
+ }
+
+ // this params are pseudo-params that cannot be shadowed
+ if (isThisParam(variable)) {
+ continue;
+ }
+
+ // ignore variables of a class name in the class scope of ClassDeclaration
+ if (isDuplicatedClassNameVariable(variable)) {
+ continue;
+ }
+
+ // ignore variables of a class name in the class scope of ClassDeclaration
+ if (isDuplicatedEnumNameVariable(variable)) {
+ continue;
+ }
+
+ // ignore configured allowed names
+ if (isAllowed(variable)) {
+ continue;
+ }
+
+ // Gets shadowed variable.
+ const shadowed = getVariableByName(scope.upper, variable.name);
+ if (!shadowed) {
+ continue;
+ }
+
+ // ignore type value variable shadowing if configured
+ if (isTypeValueShadow(variable, shadowed)) {
+ continue;
+ }
+
+ const isESLintGlobal = 'writeable' in shadowed;
+ if (
+ (shadowed.identifiers.length > 0 ||
+ (options.builtinGlobals && isESLintGlobal)) &&
+ !isOnInitializer(variable, shadowed) &&
+ !(options.hoist !== 'all' && isInTdz(variable, shadowed))
+ ) {
+ context.report({
+ node: variable.identifiers[0],
+ messageId: 'noShadow',
+ data: {
+ name: variable.name,
+ },
+ });
+ }
+ }
+ }
+
+ return {
+ 'Program:exit'(): void {
+ const globalScope = context.getScope();
+ const stack = globalScope.childScopes.slice();
+
+ while (stack.length) {
+ const scope = stack.pop()!;
+
+ stack.push(...scope.childScopes);
+ checkForShadows(scope);
+ }
+ },
+ };
+ },
+});
diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
index 2528b8709fcd..134cefd76d1b 100644
--- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
+++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
@@ -20,7 +20,6 @@ import {
isNullableType,
nullThrows,
NullThrowsReasons,
- isMemberOrOptionalMemberExpression,
isIdentifier,
isTypeAnyType,
isTypeUnknownType,
@@ -66,6 +65,7 @@ const isLiteral = (type: ts.Type): boolean =>
export type Options = [
{
allowConstantLoopConditions?: boolean;
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
},
];
@@ -79,7 +79,9 @@ export type MessageId =
| 'literalBooleanExpression'
| 'noOverlapBooleanExpression'
| 'never'
- | 'neverOptionalChain';
+ | 'neverOptionalChain'
+ | 'noStrictNullCheck';
+
export default createRule({
name: 'no-unnecessary-condition',
meta: {
@@ -98,6 +100,9 @@ export default createRule({
allowConstantLoopConditions: {
type: 'boolean',
},
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
+ type: 'boolean',
+ },
},
additionalProperties: false,
},
@@ -120,18 +125,46 @@ export default createRule({
'Unnecessary conditional, the types have no overlap',
never: 'Unnecessary conditional, value is `never`',
neverOptionalChain: 'Unnecessary optional chain on a non-nullish value',
+ noStrictNullCheck:
+ 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
},
},
defaultOptions: [
{
allowConstantLoopConditions: false,
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
},
],
- create(context, [{ allowConstantLoopConditions }]) {
+ create(
+ context,
+ [
+ {
+ allowConstantLoopConditions,
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
+ },
+ ],
+ ) {
const service = getParserServices(context);
const checker = service.program.getTypeChecker();
const sourceCode = context.getSourceCode();
const compilerOptions = service.program.getCompilerOptions();
+ const isStrictNullChecks = isStrictCompilerOptionEnabled(
+ compilerOptions,
+ 'strictNullChecks',
+ );
+
+ if (
+ !isStrictNullChecks &&
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
+ ) {
+ context.report({
+ loc: {
+ start: { line: 0, column: 0 },
+ end: { line: 0, column: 0 },
+ },
+ messageId: 'noStrictNullCheck',
+ });
+ }
function getNodeType(node: TSESTree.Expression): ts.Type {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
@@ -286,7 +319,7 @@ export default createRule({
return;
}
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
- if (isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks')) {
+ if (isStrictNullChecks) {
const UNDEFINED = ts.TypeFlags.Undefined;
const NULL = ts.TypeFlags.Null;
const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {
@@ -440,18 +473,16 @@ export default createRule({
// ?.y // This access is considered "unnecessary" according to the types
// ```
function optionChainContainsArrayIndex(
- node: TSESTree.OptionalMemberExpression | TSESTree.OptionalCallExpression,
+ node: TSESTree.MemberExpression | TSESTree.CallExpression,
): boolean {
const lhsNode =
- node.type === AST_NODE_TYPES.OptionalCallExpression
- ? node.callee
- : node.object;
+ node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isArrayIndexExpression(lhsNode)) {
return true;
}
if (
- lhsNode.type === AST_NODE_TYPES.OptionalMemberExpression ||
- lhsNode.type === AST_NODE_TYPES.OptionalCallExpression
+ lhsNode.type === AST_NODE_TYPES.MemberExpression ||
+ lhsNode.type === AST_NODE_TYPES.CallExpression
) {
return optionChainContainsArrayIndex(lhsNode);
}
@@ -494,7 +525,7 @@ export default createRule({
// foo?.bar;
// ```
function isNullableOriginFromPrev(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ node: TSESTree.MemberExpression,
): boolean {
const prevType = getNodeType(node.object);
const property = node.property;
@@ -518,9 +549,10 @@ export default createRule({
node: TSESTree.LeftHandSideExpression,
): boolean {
const type = getNodeType(node);
- const isOwnNullable = isMemberOrOptionalMemberExpression(node)
- ? !isNullableOriginFromPrev(node)
- : true;
+ const isOwnNullable =
+ node.type === AST_NODE_TYPES.MemberExpression
+ ? !isNullableOriginFromPrev(node)
+ : true;
return (
isTypeAnyType(type) ||
isTypeUnknownType(type) ||
@@ -529,7 +561,7 @@ export default createRule({
}
function checkOptionalChain(
- node: TSESTree.OptionalMemberExpression | TSESTree.OptionalCallExpression,
+ node: TSESTree.MemberExpression | TSESTree.CallExpression,
beforeOperator: TSESTree.Node,
fix: '' | '.',
): void {
@@ -547,9 +579,7 @@ export default createRule({
}
const nodeToCheck =
- node.type === AST_NODE_TYPES.OptionalCallExpression
- ? node.callee
- : node.object;
+ node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isOptionableExpression(nodeToCheck)) {
return;
@@ -575,14 +605,12 @@ export default createRule({
}
function checkOptionalMemberExpression(
- node: TSESTree.OptionalMemberExpression,
+ node: TSESTree.MemberExpression,
): void {
checkOptionalChain(node, node.object, node.computed ? '' : '.');
}
- function checkOptionalCallExpression(
- node: TSESTree.OptionalCallExpression,
- ): void {
+ function checkOptionalCallExpression(node: TSESTree.CallExpression): void {
checkOptionalChain(node, node.callee, '');
}
@@ -595,8 +623,8 @@ export default createRule({
IfStatement: (node): void => checkNode(node.test),
LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
WhileStatement: checkIfLoopIsNecessaryConditional,
- OptionalMemberExpression: checkOptionalMemberExpression,
- OptionalCallExpression: checkOptionalCallExpression,
+ 'MemberExpression[optional = true]': checkOptionalMemberExpression,
+ 'CallExpression[optional = true]': checkOptionalCallExpression,
};
},
});
diff --git a/packages/eslint-plugin/src/rules/no-unsafe-call.ts b/packages/eslint-plugin/src/rules/no-unsafe-call.ts
index d3f6707e1221..0535bfeab316 100644
--- a/packages/eslint-plugin/src/rules/no-unsafe-call.ts
+++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts
@@ -42,7 +42,7 @@ export default util.createRule<[], MessageIds>({
}
return {
- ':matches(CallExpression, OptionalCallExpression) > *.callee'(
+ 'CallExpression > *.callee'(
node: TSESTree.CallExpression['callee'],
): void {
checkCall(node, node, 'unsafeCall');
diff --git a/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts b/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts
index c6280dea92cc..e0bb605f0f15 100644
--- a/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts
+++ b/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts
@@ -35,15 +35,13 @@ export default util.createRule({
const stateCache = new Map();
- function checkMemberExpression(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): State {
+ function checkMemberExpression(node: TSESTree.MemberExpression): State {
const cachedState = stateCache.get(node);
if (cachedState) {
return cachedState;
}
- if (util.isMemberOrOptionalMemberExpression(node.object)) {
+ if (node.object.type === AST_NODE_TYPES.MemberExpression) {
const objectState = checkMemberExpression(node.object);
if (objectState === State.Unsafe) {
// if the object is unsafe, we know this will be unsafe as well
@@ -73,8 +71,8 @@ export default util.createRule({
}
return {
- 'MemberExpression, OptionalMemberExpression': checkMemberExpression,
- ':matches(MemberExpression, OptionalMemberExpression)[computed = true] > *.property'(
+ MemberExpression: checkMemberExpression,
+ 'MemberExpression[computed = true] > *.property'(
node: TSESTree.Expression,
): void {
if (
diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts
index 692b484f45dc..bea697330063 100644
--- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts
+++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts
@@ -43,7 +43,8 @@ export default util.createRule({
);
}
return (
- node.type === AST_NODE_TYPES.OptionalCallExpression ||
+ (node.type === AST_NODE_TYPES.ChainExpression &&
+ node.expression.type === AST_NODE_TYPES.CallExpression) ||
node.type === AST_NODE_TYPES.ImportExpression
);
}
diff --git a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts
index ad7c3f417426..0aedd20cbb20 100644
--- a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts
+++ b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts
@@ -30,6 +30,8 @@ export default util.createRule({
category: 'Best Practices',
recommended: false,
},
+ deprecated: true,
+ replacedBy: ['no-unused-vars'],
schema: [
{
type: 'object',
diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts
index 2c3595f3ba27..85f806985794 100644
--- a/packages/eslint-plugin/src/rules/no-unused-vars.ts
+++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts
@@ -1,7 +1,9 @@
import {
- AST_NODE_TYPES,
TSESTree,
+ TSESLint,
+ AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
+import { PatternVisitor } from '@typescript-eslint/scope-manager';
import baseRule from 'eslint/lib/rules/no-unused-vars';
import * as util from '../util';
@@ -21,50 +23,190 @@ export default util.createRule({
schema: baseRule.meta.schema,
messages: baseRule.meta.messages,
},
- defaultOptions: [],
+ defaultOptions: [{}],
create(context) {
const rules = baseRule.create(context);
/**
- * Mark heritage clause as used
- * @param node The node currently being traversed
+ * Gets a list of TS module definitions for a specified variable.
+ * @param variable eslint-scope variable object.
*/
- function markHeritageAsUsed(node: TSESTree.Expression): void {
- switch (node.type) {
- case AST_NODE_TYPES.Identifier:
- context.markVariableAsUsed(node.name);
- break;
- case AST_NODE_TYPES.MemberExpression:
- markHeritageAsUsed(node.object);
- break;
- case AST_NODE_TYPES.CallExpression:
- markHeritageAsUsed(node.callee);
- break;
+ function getModuleNameDeclarations(
+ variable: TSESLint.Scope.Variable,
+ ): TSESTree.TSModuleDeclaration[] {
+ const moduleDeclarations: TSESTree.TSModuleDeclaration[] = [];
+
+ variable.defs.forEach(def => {
+ if (def.type === 'TSModuleName') {
+ moduleDeclarations.push(def.node);
+ }
+ });
+
+ return moduleDeclarations;
+ }
+
+ /**
+ * Determine if an identifier is referencing an enclosing name.
+ * This only applies to declarations that create their own scope (modules, functions, classes)
+ * @param ref The reference to check.
+ * @param nodes The candidate function nodes.
+ * @returns True if it's a self-reference, false if not.
+ */
+ function isBlockSelfReference(
+ ref: TSESLint.Scope.Reference,
+ nodes: TSESTree.Node[],
+ ): boolean {
+ let scope: TSESLint.Scope.Scope | null = ref.from;
+
+ while (scope) {
+ if (nodes.indexOf(scope.block) >= 0) {
+ return true;
+ }
+
+ scope = scope.upper;
}
+
+ return false;
}
- return Object.assign({}, rules, {
- 'TSTypeReference Identifier'(node: TSESTree.Identifier) {
- context.markVariableAsUsed(node.name);
+ function isExported(
+ variable: TSESLint.Scope.Variable,
+ target: AST_NODE_TYPES,
+ ): boolean {
+ // TS will require that all merged namespaces/interfaces are exported, so we only need to find one
+ return variable.defs.some(
+ def =>
+ def.node.type === target &&
+ (def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration ||
+ def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration),
+ );
+ }
+
+ return {
+ ...rules,
+ 'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'(
+ node:
+ | TSESTree.TSCallSignatureDeclaration
+ | TSESTree.TSConstructorType
+ | TSESTree.TSConstructSignatureDeclaration
+ | TSESTree.TSDeclareFunction
+ | TSESTree.TSEmptyBodyFunctionExpression
+ | TSESTree.TSFunctionType
+ | TSESTree.TSMethodSignature,
+ ): void {
+ // function type signature params create variables because they can be referenced within the signature,
+ // but they obviously aren't unused variables for the purposes of this rule.
+ for (const param of node.params) {
+ visitPattern(param, name => {
+ context.markVariableAsUsed(name.name);
+ });
+ }
},
- TSInterfaceHeritage(node: TSESTree.TSInterfaceHeritage) {
- if (node.expression) {
- markHeritageAsUsed(node.expression);
+ TSEnumDeclaration(): void {
+ // enum members create variables because they can be referenced within the enum,
+ // but they obviously aren't unused variables for the purposes of this rule.
+ const scope = context.getScope();
+ for (const variable of scope.variables) {
+ context.markVariableAsUsed(variable.name);
}
},
- TSClassImplements(node: TSESTree.TSClassImplements) {
- if (node.expression) {
- markHeritageAsUsed(node.expression);
+ TSMappedType(node): void {
+ // mapped types create a variable for their type name, but it's not necessary to reference it,
+ // so we shouldn't consider it as unused for the purpose of this rule.
+ context.markVariableAsUsed(node.typeParameter.name.name);
+ },
+ TSModuleDeclaration(): void {
+ const childScope = context.getScope();
+ const scope = util.nullThrows(
+ context.getScope().upper,
+ util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'),
+ );
+ for (const variable of scope.variables) {
+ const moduleNodes = getModuleNameDeclarations(variable);
+
+ if (
+ moduleNodes.length === 0 ||
+ // ignore unreferenced module definitions, as the base rule will report on them
+ variable.references.length === 0 ||
+ // ignore exported nodes
+ isExported(variable, AST_NODE_TYPES.TSModuleDeclaration)
+ ) {
+ continue;
+ }
+
+ // check if the only reference to a module's name is a self-reference in its body
+ // this won't be caught by the base rule because it doesn't understand TS modules
+ const isOnlySelfReferenced = variable.references.every(ref => {
+ return isBlockSelfReference(ref, moduleNodes);
+ });
+
+ if (isOnlySelfReferenced) {
+ context.report({
+ node: variable.identifiers[0],
+ messageId: 'unusedVar',
+ data: {
+ varName: variable.name,
+ action: 'defined',
+ additional: '',
+ },
+ });
+ }
}
},
- 'TSParameterProperty Identifier'(node: TSESTree.Identifier) {
- // just assume parameter properties are used
+ [[
+ 'TSParameterProperty > AssignmentPattern > Identifier.left',
+ 'TSParameterProperty > Identifier.parameter',
+ ].join(', ')](node: TSESTree.Identifier): void {
+ // just assume parameter properties are used as property usage tracking is beyond the scope of this rule
context.markVariableAsUsed(node.name);
},
- 'TSEnumMember Identifier'(node: TSESTree.Identifier) {
+ ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'(
+ node: TSESTree.Identifier,
+ ): void {
+ // this parameters should always be considered used as they're pseudo-parameters
context.markVariableAsUsed(node.name);
},
- '*[declare=true] Identifier'(node: TSESTree.Identifier) {
+ 'TSInterfaceDeclaration, TSTypeAliasDeclaration'(
+ node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
+ ): void {
+ const variable = context.getScope().set.get(node.id.name);
+ if (!variable) {
+ return;
+ }
+ if (
+ variable.references.length === 0 ||
+ // ignore exported nodes
+ isExported(variable, node.type)
+ ) {
+ return;
+ }
+
+ // check if the type is only self-referenced
+ // this won't be caught by the base rule because it doesn't understand self-referencing types
+ const isOnlySelfReferenced = variable.references.every(ref => {
+ if (
+ ref.identifier.range[0] >= node.range[0] &&
+ ref.identifier.range[1] <= node.range[1]
+ ) {
+ return true;
+ }
+ return false;
+ });
+ if (isOnlySelfReferenced) {
+ context.report({
+ node: variable.identifiers[0],
+ messageId: 'unusedVar',
+ data: {
+ varName: variable.name,
+ action: 'defined',
+ additional: '',
+ },
+ });
+ }
+ },
+
+ // TODO - this could probably be refined a bit
+ '*[declare=true] Identifier'(node: TSESTree.Identifier): void {
context.markVariableAsUsed(node.name);
const scope = context.getScope();
const { variableScope } = scope;
@@ -75,6 +217,86 @@ export default util.createRule({
}
}
},
- });
+ };
+
+ function visitPattern(
+ node: TSESTree.Node,
+ cb: (node: TSESTree.Identifier) => void,
+ ): void {
+ const visitor = new PatternVisitor({}, node, cb);
+ visitor.visit(node);
+ }
},
});
+
+/*
+
+###### TODO ######
+
+Edge cases that aren't currently handled due to laziness and them being super edgy edge cases
+
+
+--- function params referenced in typeof type refs in the function declaration ---
+--- NOTE - TS gets these cases wrong
+
+function _foo(
+ arg: number // arg should be unused
+): typeof arg {
+ return 1 as any;
+}
+
+function _bar(
+ arg: number, // arg should be unused
+ _arg2: typeof arg,
+) {}
+
+
+--- function names referenced in typeof type refs in the function declaration ---
+--- NOTE - TS gets these cases right
+
+function foo( // foo should be unused
+): typeof foo {
+ return 1 as any;
+}
+
+function bar( // bar should be unused
+ _arg: typeof bar
+) {}
+
+*/
+
+/*
+
+###### TODO ######
+
+We currently extend base `no-unused-vars` implementation because it's easier and lighter-weight.
+
+Because of this, there are a few false-negatives which won't get caught.
+We could fix these if we fork the base rule; but that's a lot of code (~650 lines) to add in.
+I didn't want to do that just yet without some real-world issues, considering these are pretty rare edge-cases.
+
+These cases are mishandled because the base rule assumes that each variable has one def, but type-value shadowing
+creates a variable with two defs
+
+--- type-only or value-only references to type/value shadowed variables ---
+--- NOTE - TS gets these cases wrong
+
+type T = 1;
+const T = 2; // this T should be unused
+
+type U = T; // this U should be unused
+const U = 3;
+
+const _V = U;
+
+
+--- partially exported type/value shadowed variables ---
+--- NOTE - TS gets these cases wrong
+
+export interface Foo {}
+const Foo = 1; // this Foo should be unused
+
+interface Bar {} // this Bar should be unused
+export const Bar = 1;
+
+*/
diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts
index cb6955e8e3d0..08d12645d6a3 100644
--- a/packages/eslint-plugin/src/rules/no-use-before-define.ts
+++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts
@@ -16,6 +16,7 @@ function parseOptions(options: string | Config | null): Required {
let enums = true;
let variables = true;
let typedefs = true;
+ let ignoreTypeReferences = true;
if (typeof options === 'string') {
functions = options !== 'nofunc';
@@ -25,54 +26,43 @@ function parseOptions(options: string | Config | null): Required {
enums = options.enums !== false;
variables = options.variables !== false;
typedefs = options.typedefs !== false;
+ ignoreTypeReferences = options.ignoreTypeReferences !== false;
}
- return { functions, classes, enums, variables, typedefs };
+ return {
+ functions,
+ classes,
+ enums,
+ variables,
+ typedefs,
+ ignoreTypeReferences,
+ };
}
/**
- * Checks whether or not a given scope is a top level scope.
- */
-function isTopLevelScope(scope: TSESLint.Scope.Scope): boolean {
- return scope.type === 'module' || scope.type === 'global';
-}
-
-/**
- * Checks whether or not a given variable declaration in an upper scope.
+ * Checks whether or not a given variable is a function declaration.
*/
-function isOuterScope(
- variable: TSESLint.Scope.Variable,
- reference: TSESLint.Scope.Reference,
-): boolean {
- if (variable.scope.variableScope === reference.from.variableScope) {
- // allow the same scope only if it's the top level global/module scope
- if (!isTopLevelScope(variable.scope.variableScope)) {
- return false;
- }
- }
-
- return true;
+function isFunction(variable: TSESLint.Scope.Variable): boolean {
+ return variable.defs[0].type === 'FunctionName';
}
/**
- * Checks whether or not a given variable is a function declaration.
+ * Checks whether or not a given variable is a type declaration.
*/
-function isFunction(variable: TSESLint.Scope.Variable): boolean {
- return variable.defs[0].type === 'FunctionName';
+function isTypedef(variable: TSESLint.Scope.Variable): boolean {
+ return variable.defs[0].type === 'Type';
}
/**
- * Checks whether or not a given variable is a enum declaration in an upper function scope.
+ * Checks whether or not a given variable is a enum declaration.
*/
function isOuterEnum(
variable: TSESLint.Scope.Variable,
reference: TSESLint.Scope.Reference,
): boolean {
- const node = variable.defs[0].node as TSESTree.Node;
-
return (
- node.type === AST_NODE_TYPES.TSEnumDeclaration &&
- isOuterScope(variable, reference)
+ variable.defs[0].type == 'TSEnumName' &&
+ variable.scope.variableScope !== reference.from.variableScope
);
}
@@ -84,7 +74,8 @@ function isOuterClass(
reference: TSESLint.Scope.Reference,
): boolean {
return (
- variable.defs[0].type === 'ClassName' && isOuterScope(variable, reference)
+ variable.defs[0].type === 'ClassName' &&
+ variable.scope.variableScope !== reference.from.variableScope
);
}
@@ -96,7 +87,8 @@ function isOuterVariable(
reference: TSESLint.Scope.Reference,
): boolean {
return (
- variable.defs[0].type === 'Variable' && isOuterScope(variable, reference)
+ variable.defs[0].type === 'Variable' &&
+ variable.scope.variableScope !== reference.from.variableScope
);
}
@@ -165,6 +157,7 @@ interface Config {
enums?: boolean;
variables?: boolean;
typedefs?: boolean;
+ ignoreTypeReferences?: boolean;
}
type Options = ['nofunc' | Config];
type MessageIds = 'noUseBeforeDefine';
@@ -196,6 +189,7 @@ export default util.createRule({
enums: { type: 'boolean' },
variables: { type: 'boolean' },
typedefs: { type: 'boolean' },
+ ignoreTypeReferences: { type: 'boolean' },
},
additionalProperties: false,
},
@@ -210,6 +204,7 @@ export default util.createRule({
enums: true,
variables: true,
typedefs: true,
+ ignoreTypeReferences: true,
},
],
create(context, optionsWithDefault) {
@@ -224,17 +219,23 @@ export default util.createRule({
variable: TSESLint.Scope.Variable,
reference: TSESLint.Scope.Reference,
): boolean {
+ if (reference.isTypeReference && options.ignoreTypeReferences) {
+ return false;
+ }
if (isFunction(variable)) {
- return !!options.functions;
+ return options.functions;
}
if (isOuterClass(variable, reference)) {
- return !!options.classes;
+ return options.classes;
}
if (isOuterVariable(variable, reference)) {
- return !!options.variables;
+ return options.variables;
}
if (isOuterEnum(variable, reference)) {
- return !!options.enums;
+ return options.enums;
+ }
+ if (isTypedef(variable)) {
+ return options.typedefs;
}
return true;
diff --git a/packages/eslint-plugin/src/rules/no-var-requires.ts b/packages/eslint-plugin/src/rules/no-var-requires.ts
index af1d5f815fd4..6b958d47ec2a 100644
--- a/packages/eslint-plugin/src/rules/no-var-requires.ts
+++ b/packages/eslint-plugin/src/rules/no-var-requires.ts
@@ -25,18 +25,19 @@ export default util.createRule({
defaultOptions: [],
create(context) {
return {
- 'CallExpression, OptionalCallExpression'(
- node: TSESTree.CallExpression | TSESTree.OptionalCallExpression,
- ): void {
+ CallExpression(node: TSESTree.CallExpression): void {
+ const parent =
+ node.parent?.type === AST_NODE_TYPES.ChainExpression
+ ? node.parent.parent
+ : node.parent;
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.OptionalCallExpression ||
- node.parent.type === AST_NODE_TYPES.TSAsExpression ||
- node.parent.type === AST_NODE_TYPES.MemberExpression)
+ parent &&
+ (parent.type === AST_NODE_TYPES.VariableDeclarator ||
+ parent.type === AST_NODE_TYPES.CallExpression ||
+ parent.type === AST_NODE_TYPES.TSAsExpression ||
+ parent.type === AST_NODE_TYPES.MemberExpression)
) {
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 49fbc236f5d4..361ad5ec2cf7 100644
--- a/packages/eslint-plugin/src/rules/prefer-for-of.ts
+++ b/packages/eslint-plugin/src/rules/prefer-for-of.ts
@@ -58,8 +58,7 @@ 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.OptionalMemberExpression) &&
+ node.right.type === AST_NODE_TYPES.MemberExpression &&
isMatchingIdentifier(node.right.property, 'length')
) {
return node.right.object;
@@ -172,8 +171,7 @@ export default util.createRule({
return (
!contains(body, id) ||
(node !== undefined &&
- (node.type === AST_NODE_TYPES.MemberExpression ||
- node.type === AST_NODE_TYPES.OptionalMemberExpression) &&
+ node.type === AST_NODE_TYPES.MemberExpression &&
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 fb34951611bd..f49b5385a0c8 100644
--- a/packages/eslint-plugin/src/rules/prefer-includes.ts
+++ b/packages/eslint-plugin/src/rules/prefer-includes.ts
@@ -126,14 +126,18 @@ export default createRule({
}
return {
- "BinaryExpression > :matches(CallExpression, OptionalCallExpression).left > :matches(MemberExpression, OptionalMemberExpression).callee[property.name='indexOf'][computed=false]"(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
+ [[
+ // a.indexOf(b) !== 1
+ "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]",
+ // a?.indexOf(b) !== 1
+ "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]",
+ ].join(', ')](node: TSESTree.MemberExpression): void {
// Check if the comparison is equivalent to `includes()`.
- const callNode = node.parent as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const compareNode = callNode.parent as TSESTree.BinaryExpression;
+ const callNode = node.parent as TSESTree.CallExpression;
+ const compareNode = (callNode.parent?.type ===
+ AST_NODE_TYPES.ChainExpression
+ ? callNode.parent.parent
+ : callNode.parent) as TSESTree.BinaryExpression;
const negative = isNegativeCheck(compareNode);
if (!negative && !isPositiveCheck(compareNode)) {
return;
@@ -184,12 +188,10 @@ export default createRule({
},
// /bar/.test(foo)
- ':matches(CallExpression, OptionalCallExpression) > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="test"][computed=false]'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'(
+ node: TSESTree.MemberExpression,
): void {
- const callNode = node.parent as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
+ const callNode = node.parent as TSESTree.CallExpression;
const text =
callNode.arguments.length === 1 ? parseRegExp(node.object) : null;
if (text == null) {
@@ -218,9 +220,7 @@ export default createRule({
argNode.type !== AST_NODE_TYPES.TemplateLiteral &&
argNode.type !== AST_NODE_TYPES.Identifier &&
argNode.type !== AST_NODE_TYPES.MemberExpression &&
- argNode.type !== AST_NODE_TYPES.OptionalMemberExpression &&
- argNode.type !== AST_NODE_TYPES.CallExpression &&
- argNode.type !== AST_NODE_TYPES.OptionalCallExpression;
+ argNode.type !== AST_NODE_TYPES.CallExpression;
yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
if (needsParen) {
@@ -229,11 +229,7 @@ export default createRule({
}
yield fixer.insertTextAfter(
argNode,
- `${
- callNode.type === AST_NODE_TYPES.OptionalCallExpression
- ? '?.'
- : '.'
- }includes(${JSON.stringify(text)}`,
+ `${node.optional ? '?.' : '.'}includes(${JSON.stringify(text)}`,
);
},
});
diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts
index 52067d2f9b78..cd886a9d9974 100644
--- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts
+++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts
@@ -8,10 +8,9 @@ import * as util from '../util';
type ValidChainTarget =
| TSESTree.BinaryExpression
| TSESTree.CallExpression
- | TSESTree.MemberExpression
- | TSESTree.OptionalCallExpression
- | TSESTree.OptionalMemberExpression
+ | TSESTree.ChainExpression
| TSESTree.Identifier
+ | TSESTree.MemberExpression
| TSESTree.ThisExpression;
/*
@@ -56,6 +55,7 @@ export default util.createRule({
[[
'LogicalExpression[operator="&&"] > Identifier',
'LogicalExpression[operator="&&"] > MemberExpression',
+ 'LogicalExpression[operator="&&"] > ChainExpression > MemberExpression',
'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]',
'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]',
].join(',')](
@@ -65,7 +65,10 @@ export default util.createRule({
| TSESTree.MemberExpression,
): void {
// selector guarantees this cast
- const initialExpression = initialIdentifierOrNotEqualsExpr.parent as TSESTree.LogicalExpression;
+ const initialExpression = (initialIdentifierOrNotEqualsExpr.parent
+ ?.type === AST_NODE_TYPES.ChainExpression
+ ? initialIdentifierOrNotEqualsExpr.parent.parent
+ : initialIdentifierOrNotEqualsExpr.parent) as TSESTree.LogicalExpression;
if (initialExpression.left !== initialIdentifierOrNotEqualsExpr) {
// the node(identifier or member expression) is not the deepest left node
@@ -192,10 +195,7 @@ export default util.createRule({
);
}
- if (
- node.type === AST_NODE_TYPES.CallExpression ||
- node.type === AST_NODE_TYPES.OptionalCallExpression
- ) {
+ if (node.type === AST_NODE_TYPES.CallExpression) {
const calleeText = getText(
// isValidChainTarget ensures this is type safe
node.callee as ValidChainTarget,
@@ -233,27 +233,33 @@ export default util.createRule({
return 'this';
}
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
+ /* istanbul ignore if */ if (
+ node.expression.type === AST_NODE_TYPES.TSNonNullExpression
+ ) {
+ // this shouldn't happen
+ return '';
+ }
+ return getText(node.expression);
+ }
+
return getMemberExpressionText(node);
}
/**
* Gets a normalized representation of the given MemberExpression
*/
- function getMemberExpressionText(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): string {
+ function getMemberExpressionText(node: TSESTree.MemberExpression): string {
let objectText: string;
// cases should match the list in ALLOWED_MEMBER_OBJECT_TYPES
switch (node.object.type) {
case AST_NODE_TYPES.CallExpression:
- case AST_NODE_TYPES.OptionalCallExpression:
case AST_NODE_TYPES.Identifier:
objectText = getText(node.object);
break;
case AST_NODE_TYPES.MemberExpression:
- case AST_NODE_TYPES.OptionalMemberExpression:
objectText = getMemberExpressionText(node.object);
break;
@@ -280,7 +286,6 @@ export default util.createRule({
break;
case AST_NODE_TYPES.MemberExpression:
- case AST_NODE_TYPES.OptionalMemberExpression:
propertyText = getMemberExpressionText(node.property);
break;
@@ -316,15 +321,12 @@ const ALLOWED_MEMBER_OBJECT_TYPES: ReadonlySet = new Set([
AST_NODE_TYPES.CallExpression,
AST_NODE_TYPES.Identifier,
AST_NODE_TYPES.MemberExpression,
- AST_NODE_TYPES.OptionalCallExpression,
- AST_NODE_TYPES.OptionalMemberExpression,
AST_NODE_TYPES.ThisExpression,
]);
const ALLOWED_COMPUTED_PROP_TYPES: ReadonlySet = new Set([
AST_NODE_TYPES.Identifier,
AST_NODE_TYPES.Literal,
AST_NODE_TYPES.MemberExpression,
- AST_NODE_TYPES.OptionalMemberExpression,
AST_NODE_TYPES.TemplateLiteral,
]);
const ALLOWED_NON_COMPUTED_PROP_TYPES: ReadonlySet = new Set([
@@ -335,10 +337,11 @@ function isValidChainTarget(
node: TSESTree.Node,
allowIdentifier: boolean,
): node is ValidChainTarget {
- if (
- node.type === AST_NODE_TYPES.MemberExpression ||
- node.type === AST_NODE_TYPES.OptionalMemberExpression
- ) {
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
+ return isValidChainTarget(node.expression, allowIdentifier);
+ }
+
+ if (node.type === AST_NODE_TYPES.MemberExpression) {
const isObjectValid =
ALLOWED_MEMBER_OBJECT_TYPES.has(node.object.type) &&
// make sure to validate the expression is of our expected structure
@@ -346,8 +349,7 @@ function isValidChainTarget(
const isPropertyValid = node.computed
? ALLOWED_COMPUTED_PROP_TYPES.has(node.property.type) &&
// make sure to validate the member expression is of our expected structure
- (node.property.type === AST_NODE_TYPES.MemberExpression ||
- node.property.type === AST_NODE_TYPES.OptionalMemberExpression
+ (node.property.type === AST_NODE_TYPES.MemberExpression
? isValidChainTarget(node.property, allowIdentifier)
: true)
: ALLOWED_NON_COMPUTED_PROP_TYPES.has(node.property.type);
@@ -355,10 +357,7 @@ function isValidChainTarget(
return isObjectValid && isPropertyValid;
}
- if (
- node.type === AST_NODE_TYPES.CallExpression ||
- node.type === AST_NODE_TYPES.OptionalCallExpression
- ) {
+ if (node.type === AST_NODE_TYPES.CallExpression) {
return isValidChainTarget(node.callee, allowIdentifier);
}
diff --git a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts
index 7b63125e7430..3b1af79ac6e8 100644
--- a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts
+++ b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts
@@ -4,13 +4,12 @@ import {
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';
-type MemberExpressionWithCallExpressionParent = (
- | TSESTree.MemberExpression
- | TSESTree.OptionalMemberExpression
-) & { parent: TSESTree.CallExpression | TSESTree.OptionalCallExpression };
+type MemberExpressionWithCallExpressionParent = TSESTree.MemberExpression & {
+ parent: TSESTree.CallExpression;
+};
const getMemberExpressionName = (
- member: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ member: TSESTree.MemberExpression,
): string | null => {
if (!member.computed) {
return member.property.name;
@@ -50,7 +49,7 @@ export default util.createRule({
const checker = service.program.getTypeChecker();
return {
- ':matches(CallExpression, OptionalCallExpression) > :matches(MemberExpression, OptionalMemberExpression).callee'(
+ 'CallExpression > MemberExpression.callee'(
callee: MemberExpressionWithCallExpressionParent,
): void {
if (getMemberExpressionName(callee) !== 'reduce') {
diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts
index 90b38f18e78d..3126f6160381 100644
--- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts
+++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts
@@ -11,6 +11,8 @@ import {
getStaticValue,
getTypeName,
isNotClosingParenToken,
+ nullThrows,
+ NullThrowsReasons,
} from '../util';
const EQ_OPERATORS = /^[=!]=/;
@@ -144,10 +146,7 @@ export default createRule({
node: TSESTree.Node,
expectedObjectNode: TSESTree.Node,
): boolean {
- if (
- node.type === AST_NODE_TYPES.MemberExpression ||
- node.type === AST_NODE_TYPES.OptionalMemberExpression
- ) {
+ if (node.type === AST_NODE_TYPES.MemberExpression) {
return (
getPropertyName(node, globalScope) === 'length' &&
isSameTokens(node.object, expectedObjectNode)
@@ -216,7 +215,7 @@ export default createRule({
* @param node The member expression node to get.
*/
function getPropertyRange(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ node: TSESTree.MemberExpression,
): [number, number] {
const dotOrOpenBracket = sourceCode.getTokenAfter(
node.object,
@@ -288,6 +287,25 @@ export default createRule({
return { isEndsWith, isStartsWith, text };
}
+ function getLeftNode(node: TSESTree.Expression): TSESTree.MemberExpression {
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
+ return getLeftNode(node.expression);
+ }
+
+ let leftNode;
+ if (node.type === AST_NODE_TYPES.CallExpression) {
+ leftNode = node.callee;
+ } else {
+ leftNode = node;
+ }
+
+ if (leftNode.type !== AST_NODE_TYPES.MemberExpression) {
+ throw new Error(`Expected a MemberExpression, got ${leftNode.type}`);
+ }
+
+ return leftNode;
+ }
+
/**
* Fix code with using the right operand as the search string.
* For example: `foo.slice(0, 3) === 'bar'` → `foo.startsWith('bar')`
@@ -304,12 +322,7 @@ export default createRule({
isOptional: boolean,
): IterableIterator {
// left is CallExpression or MemberExpression.
- const leftNode = (node.left.type === AST_NODE_TYPES.CallExpression ||
- node.left.type === AST_NODE_TYPES.OptionalCallExpression
- ? node.left.callee
- : node.left) as
- | TSESTree.MemberExpression
- | TSESTree.OptionalMemberExpression;
+ const leftNode = getLeftNode(node.left);
const propertyRange = getPropertyRange(leftNode);
if (isNegative) {
@@ -333,17 +346,12 @@ export default createRule({
function* fixWithArgument(
fixer: TSESLint.RuleFixer,
node: TSESTree.BinaryExpression,
+ callNode: TSESTree.CallExpression,
+ calleeNode: TSESTree.MemberExpression,
kind: 'start' | 'end',
negative: boolean,
isOptional: boolean,
): IterableIterator {
- const callNode = node.left as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const calleeNode = callNode.callee as
- | TSESTree.MemberExpression
- | TSESTree.OptionalMemberExpression;
-
if (negative) {
yield fixer.insertTextBefore(node, '!');
}
@@ -354,27 +362,34 @@ export default createRule({
yield fixer.removeRange([callNode.range[1], node.range[1]]);
}
+ function getParent(node: TSESTree.Node): TSESTree.Node {
+ return nullThrows(
+ node.parent?.type === AST_NODE_TYPES.ChainExpression
+ ? node.parent.parent
+ : node.parent,
+ NullThrowsReasons.MissingParent,
+ );
+ }
+
return {
// foo[0] === "a"
// foo.charAt(0) === "a"
// foo[foo.length - 1] === "a"
// foo.charAt(foo.length - 1) === "a"
[[
- 'BinaryExpression > :matches(MemberExpression, OptionalMemberExpression).left[computed=true]',
- 'BinaryExpression > :matches(CallExpression, OptionalCallExpression).left > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="charAt"][computed=false]',
- ].join(', ')](
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
- let parentNode = node.parent!;
+ 'BinaryExpression > MemberExpression.left[computed=true]',
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="charAt"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > MemberExpression[computed=true]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="charAt"][computed=false]',
+ ].join(', ')](node: TSESTree.MemberExpression): void {
+ let parentNode = getParent(node);
+
let indexNode: TSESTree.Node | null = null;
- if (
- parentNode.type === AST_NODE_TYPES.CallExpression ||
- parentNode.type === AST_NODE_TYPES.OptionalCallExpression
- ) {
+ if (parentNode?.type === AST_NODE_TYPES.CallExpression) {
if (parentNode.arguments.length === 1) {
indexNode = parentNode.arguments[0];
}
- parentNode = parentNode.parent!;
+ parentNode = getParent(parentNode);
} else {
indexNode = node.property;
}
@@ -414,18 +429,16 @@ export default createRule({
},
// foo.indexOf('bar') === 0
- 'BinaryExpression > :matches(CallExpression, OptionalCallExpression).left > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="indexOf"][computed=false]'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
- const callNode = node.parent as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const parentNode = callNode.parent!;
+ [[
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="indexOf"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="indexOf"][computed=false]',
+ ].join(', ')](node: TSESTree.MemberExpression): void {
+ const callNode = getParent(node) as TSESTree.CallExpression;
+ const parentNode = getParent(callNode);
if (
callNode.arguments.length !== 1 ||
!isEqualityComparison(parentNode) ||
- parentNode.left !== callNode ||
!isNumber(parentNode.right, 0) ||
!isStringType(node.object)
) {
@@ -439,6 +452,8 @@ export default createRule({
return fixWithArgument(
fixer,
parentNode,
+ callNode,
+ node,
'start',
parentNode.operator.startsWith('!'),
node.optional,
@@ -449,18 +464,16 @@ export default createRule({
// foo.lastIndexOf('bar') === foo.length - 3
// foo.lastIndexOf(bar) === foo.length - bar.length
- 'BinaryExpression > :matches(CallExpression, OptionalCallExpression).left > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="lastIndexOf"][computed=false]'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
- const callNode = node.parent! as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const parentNode = callNode.parent!;
+ [[
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="lastIndexOf"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="lastIndexOf"][computed=false]',
+ ].join(', ')](node: TSESTree.MemberExpression): void {
+ const callNode = getParent(node) as TSESTree.CallExpression;
+ const parentNode = getParent(callNode);
if (
callNode.arguments.length !== 1 ||
!isEqualityComparison(parentNode) ||
- parentNode.left !== callNode ||
parentNode.right.type !== AST_NODE_TYPES.BinaryExpression ||
parentNode.right.operator !== '-' ||
!isLengthExpression(parentNode.right.left, node.object) ||
@@ -477,6 +490,8 @@ export default createRule({
return fixWithArgument(
fixer,
parentNode,
+ callNode,
+ node,
'end',
parentNode.operator.startsWith('!'),
node.optional,
@@ -487,13 +502,13 @@ export default createRule({
// foo.match(/^bar/) === null
// foo.match(/bar$/) === null
- 'BinaryExpression > :matches(CallExpression, OptionalCallExpression).left > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="match"][computed=false]'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
- const callNode = node.parent as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const parentNode = callNode.parent as TSESTree.BinaryExpression;
+ [[
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="match"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="match"][computed=false]',
+ ].join(', ')](node: TSESTree.MemberExpression): void {
+ const callNode = getParent(node) as TSESTree.CallExpression;
+ const parentNode = getParent(callNode) as TSESTree.BinaryExpression;
+
if (
!isEqualityComparison(parentNode) ||
!isNull(parentNode.right) ||
@@ -540,20 +555,15 @@ export default createRule({
// foo.substring(foo.length - 3) === 'bar'
// foo.substring(foo.length - 3, foo.length) === 'bar'
[[
- ':matches(CallExpression, OptionalCallExpression) > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="slice"][computed=false]',
- ':matches(CallExpression, OptionalCallExpression) > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="substring"][computed=false]',
- ].join(', ')](
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
- const callNode = node.parent! as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
- const parentNode = callNode.parent!;
- if (
- !isEqualityComparison(parentNode) ||
- parentNode.left !== callNode ||
- !isStringType(node.object)
- ) {
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="slice"][computed=false]',
+ 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="substring"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="slice"][computed=false]',
+ 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="substring"][computed=false]',
+ ].join(', ')](node: TSESTree.MemberExpression): void {
+ const callNode = getParent(node) as TSESTree.CallExpression;
+ const parentNode = getParent(callNode);
+
+ if (!isEqualityComparison(parentNode) || !isStringType(node.object)) {
return;
}
@@ -621,12 +631,10 @@ export default createRule({
// /^bar/.test(foo)
// /bar$/.test(foo)
- ':matches(CallExpression, OptionalCallExpression) > :matches(MemberExpression, OptionalMemberExpression).callee[property.name="test"][computed=false]'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'(
+ node: TSESTree.MemberExpression,
): void {
- const callNode = node.parent as
- | TSESTree.CallExpression
- | TSESTree.OptionalCallExpression;
+ const callNode = getParent(node) as TSESTree.CallExpression;
const parsed =
callNode.arguments.length === 1 ? parseRegExp(node.object) : null;
if (parsed == null) {
@@ -646,9 +654,7 @@ export default createRule({
argNode.type !== AST_NODE_TYPES.TemplateLiteral &&
argNode.type !== AST_NODE_TYPES.Identifier &&
argNode.type !== AST_NODE_TYPES.MemberExpression &&
- argNode.type !== AST_NODE_TYPES.OptionalMemberExpression &&
- argNode.type !== AST_NODE_TYPES.CallExpression &&
- argNode.type !== AST_NODE_TYPES.OptionalCallExpression;
+ argNode.type !== AST_NODE_TYPES.CallExpression;
yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
if (needsParen) {
@@ -657,11 +663,9 @@ export default createRule({
}
yield fixer.insertTextAfter(
argNode,
- `${
- callNode.type === AST_NODE_TYPES.OptionalCallExpression
- ? '?.'
- : '.'
- }${methodName}(${JSON.stringify(text)}`,
+ `${node.optional ? '?.' : '.'}${methodName}(${JSON.stringify(
+ text,
+ )}`,
);
},
});
diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts
index d18f2bec14a2..6e0c49f31a8b 100644
--- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts
+++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts
@@ -62,8 +62,8 @@ export default util.createRule({
}
return {
- ":matches(CallExpression, OptionalCallExpression)[arguments.length=0] > :matches(MemberExpression, OptionalMemberExpression)[property.name='sort'][computed=false]"(
- callee: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
+ "CallExpression[arguments.length=0] > MemberExpression[property.name='sort'][computed=false]"(
+ callee: TSESTree.MemberExpression,
): void {
const tsNode = service.esTreeNodeToTSNodeMap.get(callee.object);
const calleeObjType = util.getConstrainedTypeAtLocation(
diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
index 747cbefb7ebe..51265feb1368 100644
--- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
+++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
@@ -15,6 +15,7 @@ export type Options = [
allowNullableString?: boolean;
allowNullableNumber?: boolean;
allowAny?: boolean;
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
},
];
@@ -28,7 +29,8 @@ export type MessageId =
| 'conditionErrorNumber'
| 'conditionErrorNullableNumber'
| 'conditionErrorObject'
- | 'conditionErrorNullableObject';
+ | 'conditionErrorNullableObject'
+ | 'noStrictNullCheck';
export default util.createRule({
name: 'strict-boolean-expressions',
@@ -51,6 +53,9 @@ export default util.createRule({
allowNullableString: { type: 'boolean' },
allowNullableNumber: { type: 'boolean' },
allowAny: { type: 'boolean' },
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
+ type: 'boolean',
+ },
},
additionalProperties: false,
},
@@ -86,6 +91,8 @@ export default util.createRule({
conditionErrorNullableObject:
'Unexpected nullable object value in conditional. ' +
'An explicit null check is required.',
+ noStrictNullCheck:
+ 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
},
},
defaultOptions: [
@@ -93,11 +100,34 @@ export default util.createRule({
allowString: true,
allowNumber: true,
allowNullableObject: true,
+ allowNullableBoolean: false,
+ allowNullableString: false,
+ allowNullableNumber: false,
+ allowAny: false,
+ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
},
],
create(context, [options]) {
const service = util.getParserServices(context);
const checker = service.program.getTypeChecker();
+ const compilerOptions = service.program.getCompilerOptions();
+ const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
+ compilerOptions,
+ 'strictNullChecks',
+ );
+
+ if (
+ !isStrictNullChecks &&
+ options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
+ ) {
+ context.report({
+ loc: {
+ start: { line: 0, column: 0 },
+ end: { line: 0, column: 0 },
+ },
+ messageId: 'noStrictNullCheck',
+ });
+ }
const checkedNodes = new Set();
diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts
index 9545364c839c..1d66cff42902 100644
--- a/packages/eslint-plugin/src/rules/typedef.ts
+++ b/packages/eslint-plugin/src/rules/typedef.ts
@@ -50,10 +50,14 @@ export default util.createRule<[Options], MessageIds>({
},
defaultOptions: [
{
- [OptionKeys.ArrowParameter]: true,
- [OptionKeys.MemberVariableDeclaration]: true,
- [OptionKeys.Parameter]: true,
- [OptionKeys.PropertyDeclaration]: true,
+ [OptionKeys.ArrayDestructuring]: false,
+ [OptionKeys.ArrowParameter]: false,
+ [OptionKeys.MemberVariableDeclaration]: false,
+ [OptionKeys.ObjectDestructuring]: false,
+ [OptionKeys.Parameter]: false,
+ [OptionKeys.PropertyDeclaration]: false,
+ [OptionKeys.VariableDeclaration]: false,
+ [OptionKeys.VariableDeclarationIgnoreFunction]: false,
},
],
create(context, [options]) {
diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts
index e8217348a4a6..246023fce104 100644
--- a/packages/eslint-plugin/src/rules/unbound-method.ts
+++ b/packages/eslint-plugin/src/rules/unbound-method.ts
@@ -118,9 +118,8 @@ const isNotImported = (
const getNodeName = (node: TSESTree.Node): string | null =>
node.type === AST_NODE_TYPES.Identifier ? node.name : null;
-const getMemberFullName = (
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
-): string => `${getNodeName(node.object)}.${getNodeName(node.property)}`;
+const getMemberFullName = (node: TSESTree.MemberExpression): string =>
+ `${getNodeName(node.object)}.${getNodeName(node.property)}`;
export default util.createRule({
name: 'unbound-method',
@@ -162,9 +161,7 @@ export default util.createRule({
);
return {
- 'MemberExpression, OptionalMemberExpression'(
- node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
- ): void {
+ MemberExpression(node: TSESTree.MemberExpression): void {
if (isSafeUse(node)) {
return;
}
@@ -271,14 +268,12 @@ function isSafeUse(node: TSESTree.Node): boolean {
case AST_NODE_TYPES.IfStatement:
case AST_NODE_TYPES.ForStatement:
case AST_NODE_TYPES.MemberExpression:
- case AST_NODE_TYPES.OptionalMemberExpression:
case AST_NODE_TYPES.SwitchStatement:
case AST_NODE_TYPES.UpdateExpression:
case AST_NODE_TYPES.WhileStatement:
return true;
case AST_NODE_TYPES.CallExpression:
- case AST_NODE_TYPES.OptionalCallExpression:
return parent.callee === node;
case AST_NODE_TYPES.ConditionalExpression:
@@ -299,6 +294,7 @@ function isSafeUse(node: TSESTree.Node): boolean {
case AST_NODE_TYPES.AssignmentExpression:
return parent.operator === '=' && node === parent.left;
+ case AST_NODE_TYPES.ChainExpression:
case AST_NODE_TYPES.TSNonNullExpression:
case AST_NODE_TYPES.TSAsExpression:
case AST_NODE_TYPES.TSTypeAssertion:
diff --git a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts
index 466e7d207eba..d2c3a684c868 100644
--- a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts
+++ b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts
@@ -194,10 +194,9 @@ function doesImmediatelyReturnFunctionExpression({
function isFunctionArgument(
parent: TSESTree.Node,
callee?: FunctionExpression,
-): parent is TSESTree.CallExpression | TSESTree.OptionalCallExpression {
+): parent is TSESTree.CallExpression {
return (
- (parent.type === AST_NODE_TYPES.CallExpression ||
- parent.type === AST_NODE_TYPES.OptionalCallExpression) &&
+ parent.type === AST_NODE_TYPES.CallExpression &&
// make sure this isn't an IIFE
parent.callee !== callee
);
diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts
index 1d8ae897d31c..103caeb58ad1 100644
--- a/packages/eslint-plugin/tests/docs.test.ts
+++ b/packages/eslint-plugin/tests/docs.test.ts
@@ -26,7 +26,8 @@ function parseReadme(): {
// find the table
const rulesTables = readme.filter(
- (token): token is marked.Tokens.Table => token.type === 'table',
+ (token): token is marked.Tokens.Table =>
+ 'type' in token && token.type === 'table',
);
if (rulesTables.length !== 2) {
throw Error('Could not find both rules tables in README.md');
diff --git a/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts b/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts
index e67ec96aed2c..6d19021fa560 100644
--- a/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts
+++ b/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts
@@ -1,5 +1,5 @@
import rule from 'eslint/lib/rules/arrow-parens';
-import { RuleTester } from '../RuleTester';
+import { RuleTester, noFormat } from '../RuleTester';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
@@ -8,7 +8,7 @@ const ruleTester = new RuleTester({
ruleTester.run('arrow-parens', rule, {
valid: [
// https://github.com/typescript-eslint/typescript-eslint/issues/14
- 'const foo = (t) => {};',
+ noFormat`const foo = (t) => {};`,
'const foo = (t) => {};',
'const foo = (t: T) => {};',
'const foo = ((t: T) => {});',
@@ -16,7 +16,7 @@ ruleTester.run('arrow-parens', rule, {
`
const foo = (bar: any): void => {
// Do nothing
-}
+};
`,
{
code: 'const foo = t => {};',
diff --git a/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts
index f9c0817772a2..0dc4807edb15 100644
--- a/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts
+++ b/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts
@@ -15,7 +15,7 @@ ruleTester.run('no-dupe-args', rule, {
// https://github.com/eslint/typescript-eslint-parser/issues/535
`
function foo({ bar }: { bar: string }) {
- console.log(bar);
+ console.log(bar);
}
`,
],
diff --git a/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts
index 57a1c42e03eb..2fc971bdef60 100644
--- a/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts
+++ b/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts
@@ -14,7 +14,7 @@ ruleTester.run('no-implicit-globals', rule, {
// https://github.com/typescript-eslint/typescript-eslint/issues/23
`
function foo() {
- return "bar";
+ return 'bar';
}
module.exports = foo;
diff --git a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts
index 21e6369d1b22..7a457a617849 100644
--- a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts
+++ b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts
@@ -16,28 +16,28 @@ ruleTester.run('no-restricted-globals', rule, {
{
code: `
export default class Test {
- private status: string;
- getStatus() {
- return this.status;
- }
+ private status: string;
+ getStatus() {
+ return this.status;
+ }
}
`,
options: ['status'],
},
{
code: `
-type Handler = (event: string) => any
+type Handler = (event: string) => any;
`,
options: ['event'],
},
{
code: `
- const a = foo?.bar?.name
+ const a = foo?.bar?.name;
`,
},
{
code: `
- const a = foo?.bar?.name ?? "foobar"
+ const a = foo?.bar?.name ?? 'foobar';
`,
},
{
@@ -58,9 +58,8 @@ function onClick() {
console.log(event);
}
-fdescribe("foo", function() {
-});
- `,
+fdescribe('foo', function () {});
+ `,
options: ['event'],
errors: [
{
@@ -73,8 +72,8 @@ fdescribe("foo", function() {
},
{
code: `
-confirm("TEST");
- `,
+confirm('TEST');
+ `,
options: ['confirm'],
errors: [
{
@@ -87,8 +86,8 @@ confirm("TEST");
},
{
code: `
-var a = confirm("TEST")?.a;
- `,
+var a = confirm('TEST')?.a;
+ `,
options: ['confirm'],
errors: [
{
diff --git a/packages/eslint-plugin/tests/eslint-rules/no-shadow.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-shadow.test.ts
deleted file mode 100644
index a1b72733b2b7..000000000000
--- a/packages/eslint-plugin/tests/eslint-rules/no-shadow.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import rule from 'eslint/lib/rules/no-shadow';
-import { RuleTester } from '../RuleTester';
-
-const ruleTester = new RuleTester({
- parserOptions: {
- ecmaVersion: 6,
- sourceType: 'module',
- ecmaFeatures: {},
- },
- parser: '@typescript-eslint/parser',
-});
-
-ruleTester.run('no-shadow', rule, {
- valid: [
- // https://github.com/eslint/typescript-eslint-parser/issues/459
- `
-type foo = any;
-function bar(foo: any) {}
- `,
- // https://github.com/typescript-eslint/typescript-eslint/issues/20
- `
-export abstract class Foo {}
-export class FooBar extends Foo {}
- `,
- // https://github.com/typescript-eslint/typescript-eslint/issues/207
- `
-function test(this: Foo) {
- function test2(this: Bar) {}
-}
- `,
- ],
- invalid: [],
-});
diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts
index 38d58b482fd3..fea74c8aae3e 100644
--- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts
+++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts
@@ -31,26 +31,35 @@ export default Beemo;
// https://github.com/eslint/typescript-eslint-parser/issues/471
`
class X {
- field = {}
+ field = {};
}
`,
// https://github.com/eslint/typescript-eslint-parser/issues/466
`
-/*globals document, selector */
-const links = document.querySelectorAll( selector ) as NodeListOf
+/*globals document, selector, NodeListOf, HTMLElement */
+const links = document.querySelectorAll(selector) as NodeListOf;
`,
+ {
+ code: `
+/*globals document, selector */
+const links = document.querySelectorAll(selector) as NodeListOf;
+ `,
+ parserOptions: {
+ lib: ['dom'],
+ },
+ },
// https://github.com/eslint/typescript-eslint-parser/issues/437
`
interface Runnable {
- run (): Result
- toString (): string
+ run(): void;
+ toString(): string;
}
`,
// https://github.com/eslint/typescript-eslint-parser/issues/416
`
export type SomeThing = {
- id: string;
-}
+ id: string;
+};
`,
// https://github.com/typescript-eslint/typescript-eslint/issues/20
`
@@ -59,9 +68,26 @@ export class FooBar extends Foo {}
`,
// https://github.com/typescript-eslint/typescript-eslint/issues/18
`
-function eachr(subject: Map): typeof subject;
-function eachr(subject: Object | Array): typeof subject {
- return subject
+interface IteratorCallback {
+ (this: Subject, value: Value, key: Key, subject: Subject): void | false;
+}
+function eachr(
+ subject: Array,
+ callback: IteratorCallback,
+): typeof subject;
+function eachr(
+ subject: Map,
+ callback: IteratorCallback,
+): typeof subject;
+function eachr