diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index e043549326a..45d6ec7652c 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -200,6 +200,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@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-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate imports | | | |
| [`@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: | |
diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-imports.md b/packages/eslint-plugin/docs/rules/no-duplicate-imports.md
new file mode 100644
index 00000000000..140a8a1ece1
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-duplicate-imports.md
@@ -0,0 +1,22 @@
+# Disallow duplicate imports (`no-duplicate-imports`)
+
+## Rule Details
+
+This rule extends the base [`eslint/no-duplicate-imports`](https://eslint.org/docs/rules/no-duplicate-imports) rule.
+This version adds support for type-only import and export.
+
+## How to use
+
+```jsonc
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "no-duplicate-imports": "off",
+ "@typescript-eslint/no-duplicate-imports": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/no-duplicate-imports` options](https://eslint.org/docs/rules/no-duplicate-imports#options).
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-duplicate-imports.md)
diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts
index 1de48a81f6e..ec298406498 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -14,6 +14,8 @@ export = {
'brace-style': 'off',
'@typescript-eslint/brace-style': 'error',
'@typescript-eslint/class-literal-property-style': 'error',
+ 'comma-dangle': 'off',
+ '@typescript-eslint/comma-dangle': 'error',
'comma-spacing': 'off',
'@typescript-eslint/comma-spacing': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error',
@@ -47,6 +49,8 @@ export = {
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': 'error',
+ 'no-duplicate-imports': 'off',
+ '@typescript-eslint/no-duplicate-imports': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
@@ -140,7 +144,5 @@ export = {
'@typescript-eslint/typedef': 'error',
'@typescript-eslint/unbound-method': 'error',
'@typescript-eslint/unified-signatures': 'error',
- 'comma-dangle': 'off',
- '@typescript-eslint/comma-dangle': 'error',
},
};
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 10d849873a0..587b79d3017 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -106,6 +106,7 @@ import typeAnnotationSpacing from './type-annotation-spacing';
import typedef from './typedef';
import unboundMethod from './unbound-method';
import unifiedSignatures from './unified-signatures';
+import noDuplicateImports from './no-duplicate-imports';
export default {
'adjacent-overload-signatures': adjacentOverloadSignatures,
@@ -212,6 +213,7 @@ export default {
'type-annotation-spacing': typeAnnotationSpacing,
'unbound-method': unboundMethod,
'unified-signatures': unifiedSignatures,
+ 'no-duplicate-imports': noDuplicateImports,
indent: indent,
quotes: quotes,
semi: semi,
diff --git a/packages/eslint-plugin/src/rules/no-duplicate-imports.ts b/packages/eslint-plugin/src/rules/no-duplicate-imports.ts
new file mode 100644
index 00000000000..d539845746f
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-duplicate-imports.ts
@@ -0,0 +1,118 @@
+import {
+ AST_NODE_TYPES,
+ TSESTree,
+} from '@typescript-eslint/experimental-utils';
+import baseRule from 'eslint/lib/rules/no-duplicate-imports';
+import * as util from '../util';
+
+type Options = util.InferOptionsTypeFromRule;
+type MessageIds = util.InferMessageIdsTypeFromRule;
+
+export default util.createRule({
+ name: 'no-duplicate-imports',
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Disallow duplicate imports',
+ category: 'Best Practices',
+ recommended: false,
+ extendsBaseRule: true,
+ },
+ schema: baseRule.meta.schema,
+ messages: {
+ ...baseRule.meta.messages,
+ importType: '{{module}} type import is duplicated',
+ importTypeAs: '{{module}} type import is duplicated as type export',
+ exportType: '{{module}} type export is duplicated',
+ exportTypeAs: '{{module}} type export is duplicated as type import',
+ },
+ },
+ defaultOptions: [
+ {
+ includeExports: false,
+ },
+ ],
+ create(context, [option]) {
+ const rules = baseRule.create(context);
+ const includeExports = option.includeExports;
+ const typeImports = new Set();
+ const typeExports = new Set();
+
+ function report(
+ messageId: MessageIds,
+ node: TSESTree.Node,
+ module: string,
+ ): void {
+ context.report({
+ messageId,
+ node,
+ data: {
+ module,
+ },
+ });
+ }
+
+ function isStringLiteral(
+ node: TSESTree.Node | null,
+ ): node is TSESTree.StringLiteral {
+ return (
+ !!node &&
+ node.type === AST_NODE_TYPES.Literal &&
+ typeof node.value === 'string'
+ );
+ }
+
+ function checkTypeImport(node: TSESTree.ImportDeclaration): void {
+ if (isStringLiteral(node.source)) {
+ const value = node.source.value;
+ if (typeImports.has(value)) {
+ report('importType', node, value);
+ }
+ if (includeExports && typeExports.has(value)) {
+ report('importTypeAs', node, value);
+ }
+ typeImports.add(value);
+ }
+ }
+
+ function checkTypeExport(
+ node: TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration,
+ ): void {
+ if (isStringLiteral(node.source)) {
+ const value = node.source.value;
+ if (typeExports.has(value)) {
+ report('exportType', node, value);
+ }
+ if (typeImports.has(value)) {
+ report('exportTypeAs', node, value);
+ }
+ typeExports.add(value);
+ }
+ }
+
+ return {
+ ...rules,
+ ImportDeclaration(node): void {
+ if (node.importKind === 'type') {
+ checkTypeImport(node);
+ return;
+ }
+ rules.ImportDeclaration(node);
+ },
+ ExportNamedDeclaration(node): void {
+ if (includeExports && node.exportKind === 'type') {
+ checkTypeExport(node);
+ return;
+ }
+ rules.ExportNamedDeclaration?.(node);
+ },
+ ExportAllDeclaration(node): void {
+ if (includeExports && node.exportKind === 'type') {
+ checkTypeExport(node);
+ return;
+ }
+ rules.ExportAllDeclaration?.(node);
+ },
+ };
+ },
+});
diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-imports.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-imports.test.ts
new file mode 100644
index 00000000000..d133a7aadd9
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-duplicate-imports.test.ts
@@ -0,0 +1,153 @@
+import rule from '../../src/rules/no-duplicate-imports';
+import { RuleTester } from '../RuleTester';
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+});
+
+ruleTester.run('no-dupe-class-members', rule, {
+ valid: [
+ {
+ code: "import type foo from 'foo';",
+ },
+ {
+ code: "import type { foo } from 'foo';",
+ },
+ {
+ code: `
+ import foo from 'foo';
+ import type bar from 'foo';
+ `,
+ },
+ {
+ code: `
+ import { foo } from 'foo';
+ import type { bar } from 'foo';
+ `,
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ export type foo = foo;
+ `,
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ export type { foo };
+ `,
+ },
+ {
+ code: `
+ export { foo } from 'foo';
+ export type { foo } from 'foo';
+ `,
+ },
+ {
+ code: `
+ export type * as foo from 'foo';
+ export type * as bar from 'foo';
+ `,
+ },
+ {
+ code: `
+ import type { bar } from 'foo';
+ export type { foo } from 'foo';
+ `,
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ export type { bar } from 'bar';
+ `,
+ options: [{ includeExports: true }],
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ export type { bar };
+ `,
+ options: [{ includeExports: true }],
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ import type foo from 'foo';
+ import type bar from 'foo';
+ `,
+ errors: [
+ {
+ messageId: 'importType',
+ data: {
+ module: 'foo',
+ },
+ },
+ ],
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ import type { bar } from 'foo';
+ `,
+ errors: [{ messageId: 'importType' }],
+ },
+ {
+ code: `
+ export type { foo } from 'foo';
+ import type { bar } from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'importTypeAs' }],
+ },
+ {
+ code: `
+ import type foo from 'foo';
+ export type * from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'exportTypeAs' }],
+ },
+ {
+ code: `
+ import type { foo } from 'foo';
+ export type { foo } from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'exportTypeAs' }],
+ },
+ {
+ code: `
+ export type * as foo from 'foo';
+ export type * as bar from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'exportType' }],
+ },
+
+ // check base rule
+ {
+ code: `
+ import foo from 'foo';
+ import bar from 'foo';
+ `,
+ errors: [{ messageId: 'import' }],
+ },
+ {
+ code: `
+ import foo from 'foo';
+ export * from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'exportAs' }],
+ },
+ {
+ code: `
+ import foo from 'foo';
+ export { foo } from 'foo';
+ `,
+ options: [{ includeExports: true }],
+ errors: [{ messageId: 'exportAs' }],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts
index 39d49db7328..d11c5589b38 100644
--- a/packages/eslint-plugin/typings/eslint-rules.d.ts
+++ b/packages/eslint-plugin/typings/eslint-rules.d.ts
@@ -762,3 +762,29 @@ declare module 'eslint/lib/rules/comma-dangle' {
>;
export = rule;
}
+
+declare module 'eslint/lib/rules/no-duplicate-imports' {
+ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
+
+ const rule: TSESLint.RuleModule<
+ | 'import'
+ | 'importAs'
+ | 'export'
+ | 'exportAs'
+ | 'importType'
+ | 'importTypeAs'
+ | 'exportType'
+ | 'exportTypeAs',
+ [
+ {
+ includeExports?: boolean;
+ },
+ ],
+ {
+ ImportDeclaration(node: TSESTree.ImportDeclaration): void;
+ ExportNamedDeclaration?(node: TSESTree.ExportNamedDeclaration): void;
+ ExportAllDeclaration?(node: TSESTree.ExportAllDeclaration): void;
+ }
+ >;
+ export = rule;
+}