Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
packages/eslint-plugin/docs/rules/consistent-type-imports.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
packages/eslint-plugin/src/rules/consistent-type-imports.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}); | ||
}, | ||
} | ||
: {}), | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.