Skip to content

Commit

Permalink
feat(eslint-plugin): add consistent-type-imports rule
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Aug 7, 2020
1 parent 3529589 commit 182bf9a
Show file tree
Hide file tree
Showing 6 changed files with 459 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: | | |
Expand Down
52 changes: 52 additions & 0 deletions packages/eslint-plugin/docs/rules/consistent-type-imports.md
@@ -0,0 +1,52 @@
# Enforces consistent usage of type imports (`consistent-type-imports`)

## Rule Details

This rule aims to standardize the use of type imports style across the codebase.

```ts
import type { Foo } from './foo';
let foo: Foo;
```

```ts
import { Foo } from './foo';
let foo: Foo;
```

```ts
let foo: import('foo').Foo;
```

## Options

```ts
type Options =
| 'type-imports'
| 'no-type-imports'
| {
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 '...'`.

### `disallowTypeAnnotations`

If `true`, type imports in type annotations (`import()`) is not allowed.
Default is `true`.

## When Not To Use It

If you specifically want to use both import kinds for stylistic reasons, you can disable this rule.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -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',
Expand Down
153 changes: 153 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-type-imports.ts
@@ -0,0 +1,153 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import * as util from '../util';

type Prefer = 'type-imports' | 'no-type-imports';

type Options = [
| Prefer
| {
prefer?: Prefer;
disallowTypeAnnotations?: boolean;
},
];
type MessageIds = 'typeOverValue' | 'valueOverType' | 'noImportTypeAnnotations';
export default util.createRule<Options, MessageIds>({
name: 'consistent-type-imports',
meta: {
type: 'suggestion',
docs: {
description: 'Enforces consistent usage of type imports',
category: 'Stylistic Issues',
recommended: false,
},
messages: {
typeOverValue: 'Use an `import type` instead of an `import`.',
valueOverType: 'Use an `import` instead of an `import type`.',
noImportTypeAnnotations: '`import()` type annotations are forbidden.',
},
schema: [
{
oneOf: [
{
enum: ['type-imports', 'no-type-imports'],
},
{
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 =
(typeof option === 'string' ? option : option.prefer) ?? 'type-imports';
const disallowTypeAnnotations =
typeof option === 'string'
? true
: option.disallowTypeAnnotations !== false;
const sourceCode = context.getSourceCode();

const allValueImports: TSESTree.ImportDeclaration[] = [];
const referenceIdToDeclMap = new Map<
TSESTree.Identifier,
TSESTree.ImportDeclaration
>();
return {
...(prefer === 'type-imports'
? {
// prefer type imports
'ImportDeclaration[importKind=value]'(
node: TSESTree.ImportDeclaration,
): void {
let used = false;
for (const specifier of node.specifiers) {
const id = specifier.local;
const variable = context
.getScope()
.variables.find(v => v.name === id.name)!;
for (const ref of variable.references) {
if (ref.identifier !== id) {
referenceIdToDeclMap.set(ref.identifier, node);
used = true;
}
}
}
if (used) {
allValueImports.push(node);
}
},
'TSTypeReference Identifier'(node: TSESTree.Identifier): void {
// Remove type reference ids
referenceIdToDeclMap.delete(node);
},
'Program:exit'(): void {
const usedAsValueImports = new Set(referenceIdToDeclMap.values());
for (const valueImport of allValueImports) {
if (usedAsValueImports.has(valueImport)) {
continue;
}
context.report({
node: valueImport,
messageId: 'typeOverValue',
fix(fixer) {
// import type Foo from 'foo'
// ^^^^^ insert
const importToken = sourceCode.getFirstToken(valueImport)!;
return fixer.insertTextAfter(importToken, ' type');
},
});
}
},
}
: {
// prefer no type imports
'ImportDeclaration[importKind=type]'(
node: TSESTree.ImportDeclaration,
): void {
context.report({
node: node,
messageId: 'valueOverType',
fix(fixer) {
// import type Foo from 'foo'
// ^^^^^ remove
const importToken = sourceCode.getFirstToken(node)!;
return fixer.removeRange([
importToken.range[1],
sourceCode.getTokenAfter(importToken)!.range[1],
]);
},
});
},
}),
...(disallowTypeAnnotations
? {
// disallow `import()` type
TSImportType(node: TSESTree.TSImportType): void {
context.report({
node: node,
messageId: 'noImportTypeAnnotations',
});
},
}
: {}),
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -115,6 +116,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,
Expand Down

0 comments on commit 182bf9a

Please sign in to comment.