Skip to content

Commit

Permalink
feat(eslint-plugin): add extension rule no-duplicate-imports (#2609)
Browse files Browse the repository at this point in the history
  • Loading branch information
yeonjuan committed Oct 4, 2020
1 parent d72951a commit 498f397
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: | |
Expand Down
22 changes: 22 additions & 0 deletions 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).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-duplicate-imports.md)</sup>
6 changes: 4 additions & 2 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
},
};
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
118 changes: 118 additions & 0 deletions 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<typeof baseRule>;
type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

export default util.createRule<Options, MessageIds>({
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);
},
};
},
});
153 changes: 153 additions & 0 deletions 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' }],
},
],
});
26 changes: 26 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -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;
}

0 comments on commit 498f397

Please sign in to comment.