Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add extension rule no-duplicate-imports #2609

Merged
merged 6 commits into from Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
Comment on lines +86 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait... is this really how the base rule works?

What is wrong with the code

import { T } from 'foo';
export { A } from 'foo';

This seems like it's much better than

import { T, A } from 'foo';
export { A };

Because the latter code defines a local variable for no reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradzacher right! This rule seems just focus on duplicate module sources.🤣

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, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi :)
Isn't this rule id should be no-duplicate-imports, not no-dupe-class-members?

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;
}