Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f61af7c

Browse files
authored
feat(eslint-plugin): [consistent-type-imports] support TS4.5 inline import specifiers (typescript-eslint#4237)
1 parent be4d976 commit f61af7c

File tree

2 files changed

+139
-43
lines changed

2 files changed

+139
-43
lines changed

packages/eslint-plugin/src/rules/consistent-type-imports.ts

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface ReportValueImport {
2828
typeSpecifiers: TSESTree.ImportClause[]; // It has at least one element.
2929
valueSpecifiers: TSESTree.ImportClause[];
3030
unusedSpecifiers: TSESTree.ImportClause[];
31+
inlineTypeSpecifiers: TSESTree.ImportSpecifier[];
3132
}
3233

3334
function isImportToken(
@@ -106,7 +107,7 @@ export default util.createRule<Options, MessageIds>({
106107
...(prefer === 'type-imports'
107108
? {
108109
// prefer type imports
109-
ImportDeclaration(node: TSESTree.ImportDeclaration): void {
110+
ImportDeclaration(node): void {
110111
const source = node.source.value;
111112
const sourceImports =
112113
sourceImportsMap[source] ??
@@ -139,9 +140,18 @@ export default util.createRule<Options, MessageIds>({
139140
}
140141

141142
const typeSpecifiers: TSESTree.ImportClause[] = [];
143+
const inlineTypeSpecifiers: TSESTree.ImportSpecifier[] = [];
142144
const valueSpecifiers: TSESTree.ImportClause[] = [];
143145
const unusedSpecifiers: TSESTree.ImportClause[] = [];
144146
for (const specifier of node.specifiers) {
147+
if (
148+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
149+
specifier.importKind === 'type'
150+
) {
151+
inlineTypeSpecifiers.push(specifier);
152+
continue;
153+
}
154+
145155
const [variable] = context.getDeclaredVariables(specifier);
146156
if (variable.references.length === 0) {
147157
unusedSpecifiers.push(specifier);
@@ -229,6 +239,7 @@ export default util.createRule<Options, MessageIds>({
229239
typeSpecifiers,
230240
valueSpecifiers,
231241
unusedSpecifiers,
242+
inlineTypeSpecifiers,
232243
});
233244
}
234245
},
@@ -247,7 +258,11 @@ export default util.createRule<Options, MessageIds>({
247258
node: report.node,
248259
messageId: 'typeOverValue',
249260
*fix(fixer) {
250-
yield* fixToTypeImport(fixer, report, sourceImports);
261+
yield* fixToTypeImportDeclaration(
262+
fixer,
263+
report,
264+
sourceImports,
265+
);
251266
},
252267
});
253268
} else {
@@ -298,13 +313,17 @@ export default util.createRule<Options, MessageIds>({
298313
...message,
299314
*fix(fixer) {
300315
if (isTypeImport) {
301-
yield* fixToValueImportInDecoMeta(
316+
yield* fixToValueImportDeclaration(
302317
fixer,
303318
report,
304319
sourceImports,
305320
);
306321
} else {
307-
yield* fixToTypeImport(fixer, report, sourceImports);
322+
yield* fixToTypeImportDeclaration(
323+
fixer,
324+
report,
325+
sourceImports,
326+
);
308327
}
309328
},
310329
});
@@ -322,7 +341,21 @@ export default util.createRule<Options, MessageIds>({
322341
node,
323342
messageId: 'valueOverType',
324343
fix(fixer) {
325-
return fixToValueImport(fixer, node);
344+
return fixRemoveTypeSpecifierFromImportDeclaration(
345+
fixer,
346+
node,
347+
);
348+
},
349+
});
350+
},
351+
'ImportSpecifier[importKind = "type"]'(
352+
node: TSESTree.ImportSpecifier,
353+
): void {
354+
context.report({
355+
node,
356+
messageId: 'valueOverType',
357+
fix(fixer) {
358+
return fixRemoveTypeSpecifierFromImportSpecifier(fixer, node);
326359
},
327360
});
328361
},
@@ -345,20 +378,19 @@ export default util.createRule<Options, MessageIds>({
345378
namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null;
346379
namedSpecifiers: TSESTree.ImportSpecifier[];
347380
} {
348-
const defaultSpecifier: TSESTree.ImportDefaultSpecifier | null =
381+
const defaultSpecifier =
349382
node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier
350383
? node.specifiers[0]
351384
: null;
352-
const namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null =
385+
const namespaceSpecifier =
353386
node.specifiers.find(
354387
(specifier): specifier is TSESTree.ImportNamespaceSpecifier =>
355388
specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
356389
) ?? null;
357-
const namedSpecifiers: TSESTree.ImportSpecifier[] =
358-
node.specifiers.filter(
359-
(specifier): specifier is TSESTree.ImportSpecifier =>
360-
specifier.type === AST_NODE_TYPES.ImportSpecifier,
361-
);
390+
const namedSpecifiers = node.specifiers.filter(
391+
(specifier): specifier is TSESTree.ImportSpecifier =>
392+
specifier.type === AST_NODE_TYPES.ImportSpecifier,
393+
);
362394
return {
363395
defaultSpecifier,
364396
namespaceSpecifier,
@@ -387,7 +419,6 @@ export default util.createRule<Options, MessageIds>({
387419
const typeNamedSpecifiersTexts: string[] = [];
388420
const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = [];
389421
if (typeNamedSpecifiers.length === allNamedSpecifiers.length) {
390-
// e.g.
391422
// import Foo, {Type1, Type2} from 'foo'
392423
// import DefType, {Type1, Type2} from 'foo'
393424
const openingBraceToken = util.nullThrows(
@@ -496,7 +527,7 @@ export default util.createRule<Options, MessageIds>({
496527
* import type { Already, Type1, Type2 } from 'foo'
497528
* ^^^^^^^^^^^^^ insert
498529
*/
499-
function insertToNamedImport(
530+
function fixInsertNamedSpecifiersInNamedSpecifierList(
500531
fixer: TSESLint.RuleFixer,
501532
target: TSESTree.ImportDeclaration,
502533
insertText: string,
@@ -511,12 +542,12 @@ export default util.createRule<Options, MessageIds>({
511542
);
512543
const before = sourceCode.getTokenBefore(closingBraceToken)!;
513544
if (!util.isCommaToken(before) && !util.isOpeningBraceToken(before)) {
514-
insertText = ',' + insertText;
545+
insertText = `,${insertText}`;
515546
}
516-
return fixer.insertTextBefore(closingBraceToken, insertText);
547+
return fixer.insertTextBefore(closingBraceToken, `${insertText}`);
517548
}
518549

519-
function* fixToTypeImport(
550+
function* fixToTypeImportDeclaration(
520551
fixer: TSESLint.RuleFixer,
521552
report: ReportValueImport,
522553
sourceImports: SourceImports,
@@ -527,19 +558,17 @@ export default util.createRule<Options, MessageIds>({
527558
classifySpecifier(node);
528559

529560
if (namespaceSpecifier && !defaultSpecifier) {
530-
// e.g.
531561
// import * as types from 'foo'
532-
yield* fixToTypeImportByInsertType(fixer, node, false);
562+
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
533563
return;
534564
} else if (defaultSpecifier) {
535565
if (
536566
report.typeSpecifiers.includes(defaultSpecifier) &&
537567
namedSpecifiers.length === 0 &&
538568
!namespaceSpecifier
539569
) {
540-
// e.g.
541570
// import Type from 'foo'
542-
yield* fixToTypeImportByInsertType(fixer, node, true);
571+
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, true);
543572
return;
544573
}
545574
} else {
@@ -549,9 +578,8 @@ export default util.createRule<Options, MessageIds>({
549578
) &&
550579
!namespaceSpecifier
551580
) {
552-
// e.g.
553581
// import {Type1, Type2} from 'foo'
554-
yield* fixToTypeImportByInsertType(fixer, node, false);
582+
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
555583
return;
556584
}
557585
}
@@ -569,11 +597,12 @@ export default util.createRule<Options, MessageIds>({
569597
const afterFixes: TSESLint.RuleFix[] = [];
570598
if (typeNamedSpecifiers.length) {
571599
if (sourceImports.typeOnlyNamedImport) {
572-
const insertTypeNamedSpecifiers = insertToNamedImport(
573-
fixer,
574-
sourceImports.typeOnlyNamedImport,
575-
fixesNamedSpecifiers.typeNamedSpecifiersText,
576-
);
600+
const insertTypeNamedSpecifiers =
601+
fixInsertNamedSpecifiersInNamedSpecifierList(
602+
fixer,
603+
sourceImports.typeOnlyNamedImport,
604+
fixesNamedSpecifiers.typeNamedSpecifiersText,
605+
);
577606
if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) {
578607
yield insertTypeNamedSpecifiers;
579608
} else {
@@ -594,7 +623,6 @@ export default util.createRule<Options, MessageIds>({
594623
namespaceSpecifier &&
595624
report.typeSpecifiers.includes(namespaceSpecifier)
596625
) {
597-
// e.g.
598626
// import Foo, * as Type from 'foo'
599627
// import DefType, * as Type from 'foo'
600628
// import DefType, * as Type from 'foo'
@@ -665,7 +693,7 @@ export default util.createRule<Options, MessageIds>({
665693
yield* afterFixes;
666694
}
667695

668-
function* fixToTypeImportByInsertType(
696+
function* fixInsertTypeSpecifierForImportDeclaration(
669697
fixer: TSESLint.RuleFixer,
670698
node: TSESTree.ImportDeclaration,
671699
isDefaultImport: boolean,
@@ -722,9 +750,19 @@ export default util.createRule<Options, MessageIds>({
722750
}
723751
}
724752
}
753+
754+
// make sure we don't do anything like `import type {type T} from 'foo';`
755+
for (const specifier of node.specifiers) {
756+
if (
757+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
758+
specifier.importKind === 'type'
759+
) {
760+
yield* fixRemoveTypeSpecifierFromImportSpecifier(fixer, specifier);
761+
}
762+
}
725763
}
726764

727-
function* fixToValueImportInDecoMeta(
765+
function* fixToValueImportDeclaration(
728766
fixer: TSESLint.RuleFixer,
729767
report: ReportValueImport,
730768
sourceImports: SourceImports,
@@ -735,18 +773,16 @@ export default util.createRule<Options, MessageIds>({
735773
classifySpecifier(node);
736774

737775
if (namespaceSpecifier) {
738-
// e.g.
739776
// import type * as types from 'foo'
740-
yield* fixToValueImport(fixer, node);
777+
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
741778
return;
742779
} else if (defaultSpecifier) {
743780
if (
744781
report.valueSpecifiers.includes(defaultSpecifier) &&
745782
namedSpecifiers.length === 0
746783
) {
747-
// e.g.
748784
// import type Type from 'foo'
749-
yield* fixToValueImport(fixer, node);
785+
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
750786
return;
751787
}
752788
} else {
@@ -755,9 +791,8 @@ export default util.createRule<Options, MessageIds>({
755791
report.valueSpecifiers.includes(specifier),
756792
)
757793
) {
758-
// e.g.
759794
// import type {Type1, Type2} from 'foo'
760-
yield* fixToValueImport(fixer, node);
795+
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
761796
return;
762797
}
763798
}
@@ -775,11 +810,12 @@ export default util.createRule<Options, MessageIds>({
775810
const afterFixes: TSESLint.RuleFix[] = [];
776811
if (valueNamedSpecifiers.length) {
777812
if (sourceImports.valueOnlyNamedImport) {
778-
const insertTypeNamedSpecifiers = insertToNamedImport(
779-
fixer,
780-
sourceImports.valueOnlyNamedImport,
781-
fixesNamedSpecifiers.typeNamedSpecifiersText,
782-
);
813+
const insertTypeNamedSpecifiers =
814+
fixInsertNamedSpecifiersInNamedSpecifierList(
815+
fixer,
816+
sourceImports.valueOnlyNamedImport,
817+
fixesNamedSpecifiers.typeNamedSpecifiersText,
818+
);
783819
if (sourceImports.valueOnlyNamedImport.range[1] <= node.range[0]) {
784820
yield insertTypeNamedSpecifiers;
785821
} else {
@@ -800,7 +836,7 @@ export default util.createRule<Options, MessageIds>({
800836
yield* afterFixes;
801837
}
802838

803-
function* fixToValueImport(
839+
function* fixRemoveTypeSpecifierFromImportDeclaration(
804840
fixer: TSESLint.RuleFixer,
805841
node: TSESTree.ImportDeclaration,
806842
): IterableIterator<TSESLint.RuleFix> {
@@ -824,5 +860,20 @@ export default util.createRule<Options, MessageIds>({
824860
);
825861
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
826862
}
863+
864+
function* fixRemoveTypeSpecifierFromImportSpecifier(
865+
fixer: TSESLint.RuleFixer,
866+
node: TSESTree.ImportSpecifier,
867+
): IterableIterator<TSESLint.RuleFix> {
868+
const typeToken = util.nullThrows(
869+
sourceCode.getFirstToken(node, isTypeToken),
870+
util.NullThrowsReasons.MissingToken('type', node.type),
871+
);
872+
const afterToken = util.nullThrows(
873+
sourceCode.getTokenAfter(typeToken, { includeComments: true }),
874+
util.NullThrowsReasons.MissingToken('any token', node.type),
875+
);
876+
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
877+
}
827878
},
828879
});

packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ ruleTester.run('consistent-type-imports', rule, {
114114
`,
115115
options: [{ prefer: 'no-type-imports' }],
116116
},
117+
`
118+
import { type A, B } from 'foo';
119+
type T = A;
120+
const b = B;
121+
`,
117122
// exports
118123
`
119124
import Type from 'foo';
@@ -1517,5 +1522,45 @@ const a: Default = '';
15171522
],
15181523
parserOptions: withMetaParserOptions,
15191524
},
1525+
{
1526+
code: `
1527+
import { type A, B } from 'foo';
1528+
type T = A;
1529+
const b = B;
1530+
`,
1531+
output: `
1532+
import { A, B } from 'foo';
1533+
type T = A;
1534+
const b = B;
1535+
`,
1536+
options: [{ prefer: 'no-type-imports' }],
1537+
errors: [
1538+
{
1539+
messageId: 'valueOverType',
1540+
line: 2,
1541+
},
1542+
],
1543+
},
1544+
{
1545+
code: `
1546+
import { A, B, type C } from 'foo';
1547+
type T = A | C;
1548+
const b = B;
1549+
`,
1550+
output: noFormat`
1551+
import type { A} from 'foo';
1552+
import { B, type C } from 'foo';
1553+
type T = A | C;
1554+
const b = B;
1555+
`,
1556+
options: [{ prefer: 'type-imports' }],
1557+
errors: [
1558+
{
1559+
messageId: 'aImportIsOnlyTypes',
1560+
data: { typeImports: '"A"' },
1561+
line: 2,
1562+
},
1563+
],
1564+
},
15201565
],
15211566
});

0 commit comments

Comments
 (0)