diff --git a/.cspell.json b/.cspell.json
index daf674b70f3..6d9aecc3a60 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -77,6 +77,8 @@
"Premade",
"prettier's",
"recurse",
+ "redeclaration",
+ "redeclarations",
"redeclared",
"reimplement",
"resync",
diff --git a/.eslintrc.js b/.eslintrc.js
index 6e70052cdfd..5fbcecbe066 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/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 031db693794..502551bf8d9 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -145,7 +145,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 +184,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/no-redeclare.md b/packages/eslint-plugin/docs/rules/no-redeclare.md
new file mode 100644
index 00000000000..a794250d6f1
--- /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 00000000000..d4b5294696a
--- /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-unused-vars.md b/packages/eslint-plugin/docs/rules/no-unused-vars.md
index 44cfae690f8..5eaf167a379 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 569b8130386..dfdab4d60f7 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/package.json b/packages/eslint-plugin/package.json
index 079fefb2668..c6f95ada07d 100644
--- a/packages/eslint-plugin/package.json
+++ b/packages/eslint-plugin/package.json
@@ -43,6 +43,7 @@
},
"dependencies": {
"@typescript-eslint/experimental-utils": "3.10.1",
+ "@typescript-eslint/scope-manager": "3.10.1",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts
index c0105c354c2..10654255245 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -74,7 +74,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 +95,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/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 63acccee914..6386c317e3d 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -51,7 +51,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';
@@ -152,7 +154,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-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts
index 817d1232be2..11ed8582c75 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-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts
new file mode 100644
index 00000000000..d85cb1dc241
--- /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-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts
new file mode 100644
index 00000000000..aab60bc8163
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-shadow.ts
@@ -0,0 +1,284 @@
+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 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 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-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts
index ad7c3f41742..0aedd20cbb2 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 2c3595f3ba2..ad5d597edf5 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,189 @@ 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,
+ 'TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'(
+ node:
+ | 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 +216,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 cb6955e8e3d..08d12645d6a 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/tests/eslint-rules/arrow-parens.test.ts b/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts
index e67ec96aed2..6d19021fa56 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 f9c0817772a..0dc4807edb1 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 57a1c42e03e..2fc971bdef6 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 21e6369d1b2..7a457a61784 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 a1b72733b2b..00000000000
--- 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 38d58b482fd..fea74c8aae3 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