-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[New] consistent-type-specifier-style
: add rule
#2473
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
Merged
ljharb
merged 1 commit into
import-js:main
from
bradzacher:2469-consistent-type-specifier
Sep 7, 2022
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,216 @@ | ||
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( | ||
node, | ||
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; | ||
ljharb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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(node, sourceCode, typeSpecifiers, 'type'); | ||
const typeofImport = getImportText(node, 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.