Skip to content

Commit 52a8061

Browse files
authored
Type-only auto-import improvements (#53590)
1 parent 0ee51b9 commit 52a8061

File tree

3 files changed

+83
-23
lines changed

3 files changed

+83
-23
lines changed

src/services/codefixes/importFixes.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
isStringANonContextualKeyword,
8181
isStringLiteral,
8282
isStringLiteralLike,
83+
isTypeOnlyImportDeclaration,
8384
isTypeOnlyImportOrExportDeclaration,
8485
isUMDExportSymbol,
8586
isValidTypeOnlyAliasUseSite,
@@ -359,7 +360,6 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu
359360
importClauseOrBindingPattern,
360361
defaultImport,
361362
arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
362-
compilerOptions,
363363
preferences);
364364
});
365365

@@ -690,7 +690,24 @@ function getAddAsTypeOnly(
690690
}
691691

692692
function tryAddToExistingImport(existingImports: readonly FixAddToExistingImportInfo[], isValidTypeOnlyUseSite: boolean, checker: TypeChecker, compilerOptions: CompilerOptions): FixAddToExistingImport | undefined {
693-
return firstDefined(existingImports, ({ declaration, importKind, symbol, targetFlags }): FixAddToExistingImport | undefined => {
693+
let best: FixAddToExistingImport | undefined;
694+
for (const existingImport of existingImports) {
695+
const fix = getAddToExistingImportFix(existingImport);
696+
if (!fix) continue;
697+
const isTypeOnly = isTypeOnlyImportDeclaration(fix.importClauseOrBindingPattern);
698+
if (
699+
fix.addAsTypeOnly !== AddAsTypeOnly.NotAllowed && isTypeOnly ||
700+
fix.addAsTypeOnly === AddAsTypeOnly.NotAllowed && !isTypeOnly
701+
) {
702+
// Give preference to putting types in existing type-only imports and avoiding conversions
703+
// of import statements to/from type-only.
704+
return fix;
705+
}
706+
best ??= fix;
707+
}
708+
return best;
709+
710+
function getAddToExistingImportFix({ declaration, importKind, symbol, targetFlags }: FixAddToExistingImportInfo): FixAddToExistingImport | undefined {
694711
if (importKind === ImportKind.CommonJS || importKind === ImportKind.Namespace || declaration.kind === SyntaxKind.ImportEqualsDeclaration) {
695712
// These kinds of imports are not combinable with anything
696713
return undefined;
@@ -703,25 +720,32 @@ function tryAddToExistingImport(existingImports: readonly FixAddToExistingImport
703720
}
704721

705722
const { importClause } = declaration;
706-
if (!importClause || !isStringLiteralLike(declaration.moduleSpecifier)) return undefined;
723+
if (!importClause || !isStringLiteralLike(declaration.moduleSpecifier)) {
724+
return undefined;
725+
}
707726
const { name, namedBindings } = importClause;
708727
// A type-only import may not have both a default and named imports, so the only way a name can
709728
// be added to an existing type-only import is adding a named import to existing named bindings.
710-
if (importClause.isTypeOnly && !(importKind === ImportKind.Named && namedBindings)) return undefined;
729+
if (importClause.isTypeOnly && !(importKind === ImportKind.Named && namedBindings)) {
730+
return undefined;
731+
}
711732

712733
// N.B. we don't have to figure out whether to use the main program checker
713734
// or the AutoImportProvider checker because we're adding to an existing import; the existence of
714735
// the import guarantees the symbol came from the main program.
715736
const addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ false, symbol, targetFlags, checker, compilerOptions);
716737

717738
if (importKind === ImportKind.Default && (
718-
name || // Cannot add a default import to a declaration that already has one
739+
name || // Cannot add a default import to a declaration that already has one
719740
addAsTypeOnly === AddAsTypeOnly.Required && namedBindings // Cannot add a default import as type-only if the import already has named bindings
720-
)) return undefined;
721-
if (
722-
importKind === ImportKind.Named &&
723-
namedBindings?.kind === SyntaxKind.NamespaceImport // Cannot add a named import to a declaration that has a namespace import
724-
) return undefined;
741+
)) {
742+
return undefined;
743+
}
744+
if (importKind === ImportKind.Named &&
745+
namedBindings?.kind === SyntaxKind.NamespaceImport // Cannot add a named import to a declaration that has a namespace import
746+
) {
747+
return undefined;
748+
}
725749

726750
return {
727751
kind: ImportFixKind.AddToExisting,
@@ -730,7 +754,7 @@ function tryAddToExistingImport(existingImports: readonly FixAddToExistingImport
730754
moduleSpecifier: declaration.moduleSpecifier.text,
731755
addAsTypeOnly,
732756
};
733-
});
757+
}
734758
}
735759

736760
function createExistingImportMap(checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions) {
@@ -1235,7 +1259,6 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile:
12351259
importClauseOrBindingPattern,
12361260
importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined,
12371261
importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : emptyArray,
1238-
compilerOptions,
12391262
preferences);
12401263
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
12411264
return includeSymbolNameInDescription
@@ -1349,7 +1372,6 @@ function doAddExistingFix(
13491372
clause: ImportClause | ObjectBindingPattern,
13501373
defaultImport: Import | undefined,
13511374
namedImports: readonly Import[],
1352-
compilerOptions: CompilerOptions,
13531375
preferences: UserPreferences,
13541376
): void {
13551377
if (clause.kind === SyntaxKind.ObjectBindingPattern) {
@@ -1364,13 +1386,6 @@ function doAddExistingFix(
13641386

13651387
const promoteFromTypeOnly = clause.isTypeOnly && some([defaultImport, ...namedImports], i => i?.addAsTypeOnly === AddAsTypeOnly.NotAllowed);
13661388
const existingSpecifiers = clause.namedBindings && tryCast(clause.namedBindings, isNamedImports)?.elements;
1367-
// If we are promoting from a type-only import and `--isolatedModules` and `--preserveValueImports`
1368-
// are enabled, we need to make every existing import specifier type-only. It may be possible that
1369-
// some of them don't strictly need to be marked type-only (if they have a value meaning and are
1370-
// never used in an emitting position). These are allowed to be imported without being type-only,
1371-
// but the user has clearly already signified that they don't need them to be present at runtime
1372-
// by placing them in a type-only import. So, just mark each specifier as type-only.
1373-
const convertExistingToTypeOnly = promoteFromTypeOnly && importNameElisionDisabled(compilerOptions);
13741389

13751390
if (defaultImport) {
13761391
Debug.assert(!clause.name, "Cannot add a default import to an import clause that already has one");
@@ -1415,7 +1430,7 @@ function doAddExistingFix(
14151430
// Organize imports puts type-only import specifiers last, so if we're
14161431
// adding a non-type-only specifier and converting all the other ones to
14171432
// type-only, there's no need to ask for the insertion index - it's 0.
1418-
const insertionIndex = convertExistingToTypeOnly && !spec.isTypeOnly
1433+
const insertionIndex = promoteFromTypeOnly && !spec.isTypeOnly
14191434
? 0
14201435
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer);
14211436
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
@@ -1441,7 +1456,11 @@ function doAddExistingFix(
14411456

14421457
if (promoteFromTypeOnly) {
14431458
changes.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile));
1444-
if (convertExistingToTypeOnly && existingSpecifiers) {
1459+
if (existingSpecifiers) {
1460+
// We used to convert existing specifiers to type-only only if compiler options indicated that
1461+
// would be meaningful (see the `importNameElisionDisabled` utility function), but user
1462+
// feedback indicated a preference for preserving the type-onlyness of existing specifiers
1463+
// regardless of whether it would make a difference in emit.
14451464
for (const specifier of existingSpecifiers) {
14461465
changes.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier);
14471466
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /node_modules/react/index.d.ts
4+
//// export interface ComponentType {}
5+
//// export interface ComponentProps {}
6+
//// export declare function useState<T>(initialState: T): [T, (newState: T) => void];
7+
//// export declare function useEffect(callback: () => void, deps: any[]): void;
8+
9+
// @Filename: /main.ts
10+
//// import type { ComponentType } from "react";
11+
//// import { useState } from "react";
12+
////
13+
//// export function Component({ prop } : { prop: ComponentType }) {
14+
//// const codeIsUnimportant = useState(1);
15+
//// useEffect/*1*/(() => {}, []);
16+
//// }
17+
18+
// @Filename: /main2.ts
19+
//// import { useState } from "react";
20+
//// import type { ComponentType } from "react";
21+
////
22+
//// type _ = ComponentProps/*2*/;
23+
24+
goTo.marker("1");
25+
verify.importFixAtPosition([
26+
`import type { ComponentType } from "react";
27+
import { useEffect, useState } from "react";
28+
29+
export function Component({ prop } : { prop: ComponentType }) {
30+
const codeIsUnimportant = useState(1);
31+
useEffect(() => {}, []);
32+
}`,
33+
]);
34+
35+
goTo.marker("2");
36+
verify.importFixAtPosition([
37+
`import { useState } from "react";
38+
import type { ComponentProps, ComponentType } from "react";
39+
40+
type _ = ComponentProps;`,
41+
]);

tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
//// new B
1010

1111
goTo.file('/b.ts');
12-
verify.importFixAtPosition([`import { A, B } from './a';
12+
verify.importFixAtPosition([`import { B, type A } from './a';
1313
new B`]);

0 commit comments

Comments
 (0)