-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New]
consistent-type-specifier-style
: add rule
- Loading branch information
1 parent
c4f3cc4
commit dba8107
Showing
9 changed files
with
729 additions
and
4 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
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
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,87 @@ | ||
# import/consistent-type-specifier-style | ||
|
||
In both Flow and TypeScript you can mark an import as a type-only import by adding a "kind" marker to the import. Both languages support two positions for marker. | ||
|
||
**At the top-level** which marks all names in the import as type-only and applies to named, default, and namespace (for TypeScript) specifiers: | ||
|
||
```ts | ||
import type Foo from 'Foo'; | ||
import type {Bar} from 'Bar'; | ||
// ts only | ||
import type * as Bam from 'Bam'; | ||
// flow only | ||
import typeof Baz from 'Baz'; | ||
``` | ||
|
||
**Inline** with to the named import, which marks just the specific name in the import as type-only. An inline specifier is only valid for named specifiers, and not for default or namespace specifiers: | ||
|
||
```ts | ||
import {type Foo} from 'Foo'; | ||
// flow only | ||
import {typeof Bar} from 'Bar'; | ||
``` | ||
|
||
## Rule Details | ||
|
||
This rule either enforces or bans the use of inline type-only markers for named imports. | ||
|
||
This rule includes a fixer that will automatically convert your specifiers to the correct form - however the fixer will not respect your preferences around de-duplicating imports. If this is important to you, consider using the [`import/no-duplicates`] rule. | ||
|
||
[`import/no-duplicates`]: ./no-duplicates.md | ||
|
||
## Options | ||
|
||
The rule accepts a single string option which may be one of: | ||
|
||
- `'prefer-inline'` - enforces that named type-only specifiers are only ever written with an inline marker; and never as part of a top-level, type-only import. | ||
- `'prefer-top-level'` - enforces that named type-only specifiers only ever written as part of a top-level, type-only import; and never with an inline marker. | ||
|
||
By default the rule will use the `prefer-inline` option. | ||
|
||
## Examples | ||
|
||
### `prefer-top-level` | ||
|
||
❌ Invalid with `["error", "prefer-top-level"]` | ||
|
||
```ts | ||
import {type Foo} from 'Foo'; | ||
import Foo, {type Bar} from 'Foo'; | ||
// flow only | ||
import {typeof Foo} from 'Foo'; | ||
``` | ||
|
||
✅ Valid with `["error", "prefer-top-level"]` | ||
|
||
```ts | ||
import type {Foo} from 'Foo'; | ||
import type Foo, {Bar} from 'Foo'; | ||
// flow only | ||
import typeof {Foo} from 'Foo'; | ||
``` | ||
|
||
### `prefer-inline` | ||
|
||
❌ Invalid with `["error", "prefer-inline"]` | ||
|
||
```ts | ||
import type {Foo} from 'Foo'; | ||
import type Foo, {Bar} from 'Foo'; | ||
// flow only | ||
import typeof {Foo} from 'Foo'; | ||
``` | ||
|
||
✅ Valid with `["error", "prefer-inline"]` | ||
|
||
```ts | ||
import {type Foo} from 'Foo'; | ||
import Foo, {type Bar} from 'Foo'; | ||
// flow only | ||
import {typeof Foo} from 'Foo'; | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you aren't using Flow or TypeScript 4.5+, then this rule does not apply and need not be used. | ||
|
||
If you don't care about, and don't want to standardize how named specifiers are imported then you should not use 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
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
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,215 @@ | ||
import docsUrl from '../docsUrl'; | ||
|
||
function isComma(token) { | ||
return token.type === 'Punctuator' && token.value === ','; | ||
} | ||
|
||
function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { | ||
for (const specifier of specifiers) { | ||
// remove the trailing comma | ||
const comma = sourceCode.getTokenAfter(specifier, isComma); | ||
if (comma) { | ||
fixes.push(fixer.remove(comma)); | ||
} | ||
fixes.push(fixer.remove(specifier)); | ||
} | ||
} | ||
|
||
function getImportText( | ||
sourceCode, | ||
specifiers, | ||
kind, | ||
) { | ||
const sourceString = sourceCode.getText(node.source); | ||
if (specifiers.length === 0) { | ||
return ''; | ||
} | ||
|
||
const names = specifiers.map(s => { | ||
if (s.imported.name === s.local.name) { | ||
return s.imported.name; | ||
} | ||
return `${s.imported.name} as ${s.local.name}`; | ||
}); | ||
// insert a fresh top-level import | ||
return `import ${kind} {${names.join(', ')}} from ${sourceString};`; | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Enforce or ban the use of inline type-only markers for named imports', | ||
url: docsUrl('consistent-type-specifier-style'), | ||
}, | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
type: 'string', | ||
enum: ['prefer-inline', 'prefer-top-level'], | ||
default: 'prefer-inline', | ||
}, | ||
], | ||
}, | ||
|
||
create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
if (context.options[0] === 'prefer-inline') { | ||
return { | ||
ImportDeclaration(node) { | ||
if (node.importKind === 'value' || node.importKind == null) { | ||
// top-level value / unknown is valid | ||
return; | ||
} | ||
|
||
if ( | ||
// no specifiers (import type {} from '') have no specifiers to mark as inline | ||
node.specifiers.length === 0 || | ||
(node.specifiers.length === 1 && | ||
// default imports are both "inline" and "top-level" | ||
(node.specifiers[0].type === 'ImportDefaultSpecifier' || | ||
// namespace imports are both "inline" and "top-level" | ||
node.specifiers[0].type === 'ImportNamespaceSpecifier')) | ||
) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node, | ||
message: 'Prefer using inline {{kind}} specifiers instead of a top-level {{kind}}-only import.', | ||
data: { | ||
kind: node.importKind, | ||
}, | ||
fix(fixer) { | ||
const kindToken = sourceCode.getFirstToken(node, { skip: 1 }); | ||
|
||
return [].concat( | ||
kindToken ? fixer.remove(kindToken) : [], | ||
node.specifiers.map((specifier) => fixer.insertTextBefore(specifier, `${node.importKind} `)), | ||
); | ||
}, | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
// prefer-top-level | ||
return { | ||
ImportDeclaration(node) { | ||
if ( | ||
// already top-level is valid | ||
node.importKind === 'type' || | ||
node.importKind === 'typeof' || | ||
// no specifiers (import {} from '') cannot have inline - so is valid | ||
node.specifiers.length === 0 || | ||
(node.specifiers.length === 1 && | ||
// default imports are both "inline" and "top-level" | ||
(node.specifiers[0].type === 'ImportDefaultSpecifier' || | ||
// namespace imports are both "inline" and "top-level" | ||
node.specifiers[0].type === 'ImportNamespaceSpecifier')) | ||
) { | ||
return; | ||
} | ||
|
||
const typeSpecifiers = []; | ||
const typeofSpecifiers = []; | ||
const valueSpecifiers = []; | ||
let defaultSpecifier = null; | ||
for (const specifier of node.specifiers) { | ||
if (specifier.type === 'ImportDefaultSpecifier') { | ||
defaultSpecifier = specifier; | ||
continue; | ||
} | ||
|
||
if (specifier.importKind === 'type') { | ||
typeSpecifiers.push(specifier); | ||
} else if (specifier.importKind === 'typeof') { | ||
typeofSpecifiers.push(specifier); | ||
} else if (specifier.importKind === 'value' || specifier.importKind == null) { | ||
valueSpecifiers.push(specifier); | ||
} | ||
} | ||
|
||
const typeImport = getImportText(sourceCode, typeSpecifiers, 'type'); | ||
const typeofImport = getImportText(sourceCode, typeofSpecifiers, 'typeof'); | ||
const newImports = `${typeImport}\n${typeofImport}`.trim(); | ||
|
||
if (typeSpecifiers.length + typeofSpecifiers.length === node.specifiers.length) { | ||
// all specifiers have inline specifiers - so we replace the entire import | ||
const kind = [].concat( | ||
typeSpecifiers.length > 0 ? 'type' : [], | ||
typeofSpecifiers.length > 0 ? 'typeof' : [], | ||
); | ||
|
||
context.report({ | ||
node, | ||
message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.', | ||
data: { | ||
kind: kind.join('/'), | ||
}, | ||
fix(fixer) { | ||
return fixer.replaceText(node, newImports); | ||
}, | ||
}); | ||
} else { | ||
// remove specific specifiers and insert new imports for them | ||
for (const specifier of typeSpecifiers.concat(typeofSpecifiers)) { | ||
context.report({ | ||
node: specifier, | ||
message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.', | ||
data: { | ||
kind: specifier.importKind, | ||
}, | ||
fix(fixer) { | ||
const fixes = []; | ||
|
||
// if there are no value specifiers, then the other report fixer will be called, not this one | ||
|
||
if (valueSpecifiers.length > 0) { | ||
// import { Value, type Type } from 'mod'; | ||
|
||
// we can just remove the type specifiers | ||
removeSpecifiers(fixes, fixer, sourceCode, typeSpecifiers); | ||
removeSpecifiers(fixes, fixer, sourceCode, typeofSpecifiers); | ||
|
||
// make the import nicely formatted by also removing the trailing comma after the last value import | ||
// eg | ||
// import { Value, type Type } from 'mod'; | ||
// to | ||
// import { Value } from 'mod'; | ||
// not | ||
// import { Value, } from 'mod'; | ||
const maybeComma = sourceCode.getTokenAfter(valueSpecifiers[valueSpecifiers.length - 1]); | ||
if (isComma(maybeComma)) { | ||
fixes.push(fixer.remove(maybeComma)); | ||
} | ||
} else if (defaultSpecifier) { | ||
// import Default, { type Type } from 'mod'; | ||
|
||
// remove the entire curly block so we don't leave an empty one behind | ||
// NOTE - the default specifier *must* be the first specifier always! | ||
// so a comma exists that we also have to clean up or else it's bad syntax | ||
const comma = sourceCode.getTokenAfter(defaultSpecifier, isComma); | ||
const closingBrace = sourceCode.getTokenAfter( | ||
node.specifiers[node.specifiers.length - 1], | ||
token => token.type === 'Punctuator' && token.value === '}', | ||
); | ||
fixes.push(fixer.removeRange([ | ||
comma.range[0], | ||
closingBrace.range[1], | ||
])); | ||
} | ||
|
||
return fixes.concat( | ||
// insert the new imports after the old declaration | ||
fixer.insertTextAfter(node, `\n${newImports}`), | ||
); | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
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.