Skip to content

Commit 1e0b2f4

Browse files
authored
Code fix should prefer import type (microsoft#54255)
1 parent 4c01b2f commit 1e0b2f4

11 files changed

+179
-19
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,14 +1188,6 @@
11881188
"category": "Error",
11891189
"code": 1371
11901190
},
1191-
"Convert to type-only import": {
1192-
"category": "Message",
1193-
"code": 1373
1194-
},
1195-
"Convert all imports not used as a value to type-only imports": {
1196-
"category": "Message",
1197-
"code": 1374
1198-
},
11991191
"'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.": {
12001192
"category": "Error",
12011193
"code": 1375
@@ -7616,6 +7608,18 @@
76167608
"category": "Message",
76177609
"code": 95179
76187610
},
7611+
"Use 'import type'": {
7612+
"category": "Message",
7613+
"code": 95180
7614+
},
7615+
"Use 'type {0}'": {
7616+
"category": "Message",
7617+
"code": 95181
7618+
},
7619+
"Fix all with type-only imports": {
7620+
"category": "Message",
7621+
"code": 95182
7622+
},
76197623

76207624
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
76217625
"category": "Error",

src/services/codefixes/convertToTypeOnlyImport.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Diagnostics,
33
factory,
4+
FindAllReferences,
45
getSynthesizedDeepClone,
56
getSynthesizedDeepClones,
67
getTokenAtPosition,
@@ -9,12 +10,18 @@ import {
910
ImportSpecifier,
1011
isImportDeclaration,
1112
isImportSpecifier,
13+
isValidTypeOnlyAliasUseSite,
14+
Program,
15+
sameMap,
16+
some,
1217
SourceFile,
18+
SyntaxKind,
1319
textChanges,
1420
} from "../_namespaces/ts";
1521
import {
1622
codeFixAll,
1723
createCodeFixAction,
24+
createCodeFixActionWithoutFixAll,
1825
registerCodeFix,
1926
} from "../_namespaces/ts.codefix";
2027

@@ -30,16 +37,46 @@ registerCodeFix({
3037
const declaration = getDeclaration(context.sourceFile, context.span.start);
3138
if (declaration) {
3239
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, declaration));
33-
return [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_type_only_import, fixId, Diagnostics.Convert_all_imports_not_used_as_a_value_to_type_only_imports)];
40+
const importDeclarationChanges = declaration.kind === SyntaxKind.ImportSpecifier && canConvertImportDeclarationForSpecifier(declaration, context.sourceFile, context.program)
41+
? textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, declaration.parent.parent.parent))
42+
: undefined;
43+
const mainAction = createCodeFixAction(
44+
fixId,
45+
changes,
46+
declaration.kind === SyntaxKind.ImportSpecifier
47+
? [Diagnostics.Use_type_0, declaration.propertyName?.text ?? declaration.name.text]
48+
: Diagnostics.Use_import_type,
49+
fixId,
50+
Diagnostics.Fix_all_with_type_only_imports);
51+
52+
if (some(importDeclarationChanges)) {
53+
return [
54+
createCodeFixActionWithoutFixAll(fixId, importDeclarationChanges, Diagnostics.Use_import_type),
55+
mainAction
56+
];
57+
}
58+
return [mainAction];
3459
}
3560
return undefined;
3661
},
3762
fixIds: [fixId],
3863
getAllCodeActions: function getAllCodeActionsToConvertToTypeOnlyImport(context) {
64+
const fixedImportDeclarations: Set<ImportDeclaration> = new Set();
3965
return codeFixAll(context, errorCodes, (changes, diag) => {
40-
const declaration = getDeclaration(diag.file, diag.start);
41-
if (declaration) {
42-
doChange(changes, diag.file, declaration);
66+
const errorDeclaration = getDeclaration(diag.file, diag.start);
67+
if (errorDeclaration?.kind === SyntaxKind.ImportDeclaration && !fixedImportDeclarations.has(errorDeclaration)) {
68+
doChange(changes, diag.file, errorDeclaration);
69+
fixedImportDeclarations.add(errorDeclaration);
70+
}
71+
else if (errorDeclaration?.kind === SyntaxKind.ImportSpecifier
72+
&& !fixedImportDeclarations.has(errorDeclaration.parent.parent.parent)
73+
&& canConvertImportDeclarationForSpecifier(errorDeclaration, diag.file, context.program)
74+
) {
75+
doChange(changes, diag.file, errorDeclaration.parent.parent.parent);
76+
fixedImportDeclarations.add(errorDeclaration.parent.parent.parent);
77+
}
78+
else if (errorDeclaration?.kind === SyntaxKind.ImportSpecifier) {
79+
doChange(changes, diag.file, errorDeclaration);
4380
}
4481
});
4582
}
@@ -50,6 +87,31 @@ function getDeclaration(sourceFile: SourceFile, pos: number) {
5087
return isImportSpecifier(parent) || isImportDeclaration(parent) && parent.importClause ? parent : undefined;
5188
}
5289

90+
91+
function canConvertImportDeclarationForSpecifier(specifier: ImportSpecifier, sourceFile: SourceFile, program: Program): boolean {
92+
if (specifier.parent.parent.name) {
93+
// An import declaration with a default import and named bindings can't be type-only
94+
return false;
95+
}
96+
const nonTypeOnlySpecifiers = specifier.parent.elements.filter(e => !e.isTypeOnly);
97+
if (nonTypeOnlySpecifiers.length === 1) {
98+
// If the error specifier is on the only non-type-only specifier, we can convert the whole import
99+
return true;
100+
}
101+
// Otherwise, we need to check the usage of the other specifiers
102+
const checker = program.getTypeChecker();
103+
for (const specifier of nonTypeOnlySpecifiers) {
104+
const isUsedAsValue = FindAllReferences.Core.eachSymbolReferenceInFile(specifier.name, checker, sourceFile, usage => {
105+
return !isValidTypeOnlyAliasUseSite(usage);
106+
});
107+
if (isUsedAsValue) {
108+
return false;
109+
}
110+
}
111+
// No other specifiers are used as values, so we can convert the whole import
112+
return true;
113+
}
114+
53115
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: ImportDeclaration | ImportSpecifier) {
54116
if (isImportSpecifier(declaration)) {
55117
changes.replaceNode(sourceFile, declaration, factory.updateImportSpecifier(declaration, /*isTypeOnly*/ true, declaration.propertyName, declaration.name));
@@ -73,8 +135,13 @@ function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, de
73135
]);
74136
}
75137
else {
138+
const newNamedBindings = importClause.namedBindings?.kind === SyntaxKind.NamedImports
139+
? factory.updateNamedImports(
140+
importClause.namedBindings,
141+
sameMap(importClause.namedBindings.elements, e => factory.updateImportSpecifier(e, /*isTypeOnly*/ false, e.propertyName, e.name)))
142+
: importClause.namedBindings;
76143
const importDeclaration = factory.updateImportDeclaration(declaration, declaration.modifiers,
77-
factory.updateImportClause(importClause, /*isTypeOnly*/ true, importClause.name, importClause.namedBindings), declaration.moduleSpecifier, declaration.assertClause);
144+
factory.updateImportClause(importClause, /*isTypeOnly*/ true, importClause.name, newNamedBindings), declaration.moduleSpecifier, declaration.assertClause);
78145
changes.replaceNode(sourceFile, declaration, importDeclaration);
79146
}
80147
}

src/services/codefixes/fixUnreferenceableDecoratorMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ registerCodeFix({
4444
actions = append(actions, createCodeFixActionWithoutFixAll(fixId, namespaceChanges, Diagnostics.Convert_named_imports_to_namespace_import));
4545
}
4646
if (typeOnlyChanges.length) {
47-
actions = append(actions, createCodeFixActionWithoutFixAll(fixId, typeOnlyChanges, Diagnostics.Convert_to_type_only_import));
47+
actions = append(actions, createCodeFixActionWithoutFixAll(fixId, typeOnlyChanges, Diagnostics.Use_import_type));
4848
}
4949
return actions;
5050
},

tests/cases/fourslash/codeFixConvertToTypeOnlyImport1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
goTo.file("imports.ts");
2121
verify.codeFix({
2222
index: 0,
23-
description: ts.Diagnostics.Convert_to_type_only_import.message,
23+
description: ts.Diagnostics.Use_import_type.message,
2424
newFileContent: `import type {
2525
B,
2626
C,

tests/cases/fourslash/codeFixConvertToTypeOnlyImport2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
goTo.file("imports.ts");
1919
verify.codeFix({
2020
index: 0,
21-
description: ts.Diagnostics.Convert_to_type_only_import.message,
21+
description: ts.Diagnostics.Use_import_type.message,
2222
newFileContent: `import type A from './exports';
2323
import type { B, C } from './exports';
2424

tests/cases/fourslash/codeFixConvertToTypeOnlyImport3.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
goTo.file("imports.ts");
2727
verify.codeFixAll({
2828
fixId: "convertToTypeOnlyImport",
29-
fixAllDescription: ts.Diagnostics.Convert_all_imports_not_used_as_a_value_to_type_only_imports.message,
29+
fixAllDescription: ts.Diagnostics.Fix_all_with_type_only_imports.message,
3030
newFileContent: `import type A from './exports1';
3131
import type { B, C } from './exports1';
3232
import type D from "./exports2";

tests/cases/fourslash/codeFixConvertToTypeOnlyImport4.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
goTo.file("/a.ts");
1313
verify.codeFix({
1414
index: 0,
15-
description: ts.Diagnostics.Convert_to_type_only_import.message,
15+
description: ts.Diagnostics.Use_import_type.message,
16+
newFileContent: `import type { I, foo } from "./b";`
17+
});
18+
verify.codeFix({
19+
index: 1,
20+
description: `Use 'type I'`,
1621
newFileContent: `import { type I, foo } from "./b";`
1722
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @verbatimModuleSyntax: true
5+
// @filename: /b.ts
6+
////export interface I {}
7+
////export const foo = {};
8+
9+
// @filename: /a.ts
10+
////import { I, foo } from "./b";
11+
////foo;
12+
////export declare const i: I;
13+
14+
// ^ usage prevents 'Remove unused declaration' fix,
15+
// so that lack of `index` option asserts only one fix is available.
16+
// Specifically, we ensure no `Use 'import type'` fix is offered.
17+
18+
goTo.file("/a.ts");
19+
verify.codeFix({
20+
description: `Use 'type I'`,
21+
newFileContent: `import { type I, foo } from "./b";
22+
foo;
23+
export declare const i: I;`,
24+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @verbatimModuleSyntax: true
5+
// @filename: /b.ts
6+
////export interface I {}
7+
////export const foo = {};
8+
9+
// @filename: /a.ts
10+
//// import { I, type foo } from "./b";
11+
//// export declare const x: typeof foo;
12+
13+
goTo.file("/a.ts");
14+
verify.codeFix({
15+
index: 0,
16+
description: ts.Diagnostics.Use_import_type.message,
17+
newFileContent: `import type { I, foo } from "./b";
18+
export declare const x: typeof foo;`,
19+
});
20+
21+
verify.codeFix({
22+
index: 1,
23+
description: `Use 'type I'`,
24+
newFileContent: `import { type I, type foo } from "./b";
25+
export declare const x: typeof foo;`,
26+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @verbatimModuleSyntax: true
5+
6+
// @Filename: /b.ts
7+
////export interface I {}
8+
////export const foo = {};
9+
10+
// @Filename: /c.ts
11+
//// export interface C {}
12+
13+
// @Filename: /d.ts
14+
//// export interface D {}
15+
//// export function d() {}
16+
17+
// @Filename: /a.ts
18+
//// import { I, type foo } from "./b";
19+
//// import { C } from "./c";
20+
//// import { D, d } from "./d";
21+
//// export declare const x: typeof foo;
22+
//// d();
23+
24+
goTo.file("/a.ts");
25+
verify.codeFixAll({
26+
fixId: "convertToTypeOnlyImport",
27+
fixAllDescription: ts.Diagnostics.Fix_all_with_type_only_imports.message,
28+
newFileContent:
29+
`import type { I, foo } from "./b";
30+
import type { C } from "./c";
31+
import { type D, d } from "./d";
32+
export declare const x: typeof foo;
33+
d();`,
34+
});

0 commit comments

Comments
 (0)