Skip to content

Commit aef4e46

Browse files
authored
Add user preference for preferring type-only auto imports (#56090)
1 parent 6424e18 commit aef4e46

File tree

7 files changed

+97
-11
lines changed

7 files changed

+97
-11
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10014,6 +10014,7 @@ export interface UserPreferences {
1001410014
readonly interactiveInlayHints?: boolean;
1001510015
readonly allowRenameOfImportPath?: boolean;
1001610016
readonly autoImportFileExcludePatterns?: string[];
10017+
readonly preferTypeOnlyAutoImports?: boolean;
1001710018
readonly organizeImportsIgnoreCase?: "auto" | boolean;
1001810019
readonly organizeImportsCollation?: "ordinal" | "unicode";
1001910020
readonly organizeImportsLocale?: string;

src/harness/fourslashImpl.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3347,12 +3347,16 @@ export class TestState {
33473347
this.verifyRangeIs(expectedText, includeWhiteSpace);
33483348
}
33493349

3350-
public verifyCodeFixAll({ fixId, fixAllDescription, newFileContent, commands: expectedCommands }: FourSlashInterface.VerifyCodeFixAllOptions): void {
3351-
const fixWithId = ts.find(this.getCodeFixes(this.activeFile.fileName), a => a.fixId === fixId);
3350+
public verifyCodeFixAll({ fixId, fixAllDescription, newFileContent, commands: expectedCommands, preferences }: FourSlashInterface.VerifyCodeFixAllOptions): void {
3351+
if (this.testType === FourSlashTestType.Server && preferences) {
3352+
this.configure(preferences);
3353+
}
3354+
3355+
const fixWithId = ts.find(this.getCodeFixes(this.activeFile.fileName, /*errorCode*/ undefined, preferences), a => a.fixId === fixId);
33523356
ts.Debug.assert(fixWithId !== undefined, "No available code fix has the expected id. Fix All is not available if there is only one potentially fixable diagnostic present.", () => `Expected '${fixId}'. Available actions:\n${ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => `${a.fixName} (${a.fixId || "no fix id"})`).join("\n")}`);
33533357
ts.Debug.assertEqual(fixWithId.fixAllDescription, fixAllDescription);
33543358

3355-
const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.emptyOptions);
3359+
const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, preferences || ts.emptyOptions);
33563360
assert.deepEqual<readonly {}[] | undefined>(commands, expectedCommands);
33573361
this.verifyNewContent({ newFileContent }, changes);
33583362
}

src/harness/fourslashInterfaceImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,7 @@ export interface VerifyCodeFixAllOptions {
18691869
fixAllDescription: string;
18701870
newFileContent: NewFileContent;
18711871
commands: readonly {}[];
1872+
preferences?: ts.UserPreferences;
18721873
}
18731874

18741875
export interface VerifyRefactorOptions {

src/services/codefixes/importFixes.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu
389389
namedImports && arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
390390
namespaceLikeImport,
391391
compilerOptions,
392+
preferences,
392393
);
393394
newDeclarations = combine(newDeclarations, declarations);
394395
});
@@ -1348,6 +1349,7 @@ function codeActionForFixWorker(
13481349
namedImports,
13491350
namespaceLikeImport,
13501351
program.getCompilerOptions(),
1352+
preferences,
13511353
),
13521354
/*blankLineBetween*/ true,
13531355
preferences,
@@ -1505,7 +1507,7 @@ function doAddExistingFix(
15051507
const newSpecifiers = stableSort(
15061508
namedImports.map(namedImport =>
15071509
factory.createImportSpecifier(
1508-
(!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport),
1510+
(!clause.isTypeOnly || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport, preferences),
15091511
/*propertyName*/ undefined,
15101512
factory.createIdentifier(namedImport.name),
15111513
)
@@ -1604,32 +1606,37 @@ function needsTypeOnly({ addAsTypeOnly }: { addAsTypeOnly: AddAsTypeOnly; }): bo
16041606
return addAsTypeOnly === AddAsTypeOnly.Required;
16051607
}
16061608

1609+
function shouldUseTypeOnly(info: { addAsTypeOnly: AddAsTypeOnly; }, preferences: UserPreferences): boolean {
1610+
return needsTypeOnly(info) || !!preferences.preferTypeOnlyAutoImports && info.addAsTypeOnly !== AddAsTypeOnly.NotAllowed;
1611+
}
1612+
16071613
function getNewImports(
16081614
moduleSpecifier: string,
16091615
quotePreference: QuotePreference,
16101616
defaultImport: Import | undefined,
16111617
namedImports: readonly Import[] | undefined,
16121618
namespaceLikeImport: Import & { importKind: ImportKind.CommonJS | ImportKind.Namespace; } | undefined,
16131619
compilerOptions: CompilerOptions,
1620+
preferences: UserPreferences,
16141621
): AnyImportSyntax | readonly AnyImportSyntax[] {
16151622
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
16161623
let statements: AnyImportSyntax | readonly AnyImportSyntax[] | undefined;
16171624
if (defaultImport !== undefined || namedImports?.length) {
16181625
// `verbatimModuleSyntax` should prefer top-level `import type` -
16191626
// even though it's not an error, it would add unnecessary runtime emit.
16201627
const topLevelTypeOnly = (!defaultImport || needsTypeOnly(defaultImport)) && every(namedImports, needsTypeOnly) ||
1621-
compilerOptions.verbatimModuleSyntax &&
1628+
(compilerOptions.verbatimModuleSyntax || preferences.preferTypeOnlyAutoImports) &&
16221629
defaultImport?.addAsTypeOnly !== AddAsTypeOnly.NotAllowed &&
16231630
!some(namedImports, i => i.addAsTypeOnly === AddAsTypeOnly.NotAllowed);
16241631
statements = combine(
16251632
statements,
16261633
makeImport(
16271634
defaultImport && factory.createIdentifier(defaultImport.name),
1628-
namedImports?.map(({ addAsTypeOnly, name }) =>
1635+
namedImports?.map(namedImport =>
16291636
factory.createImportSpecifier(
1630-
!topLevelTypeOnly && addAsTypeOnly === AddAsTypeOnly.Required,
1637+
!topLevelTypeOnly && shouldUseTypeOnly(namedImport, preferences),
16311638
/*propertyName*/ undefined,
1632-
factory.createIdentifier(name),
1639+
factory.createIdentifier(namedImport.name),
16331640
)
16341641
),
16351642
moduleSpecifier,
@@ -1643,14 +1650,14 @@ function getNewImports(
16431650
const declaration = namespaceLikeImport.importKind === ImportKind.CommonJS
16441651
? factory.createImportEqualsDeclaration(
16451652
/*modifiers*/ undefined,
1646-
needsTypeOnly(namespaceLikeImport),
1653+
shouldUseTypeOnly(namespaceLikeImport, preferences),
16471654
factory.createIdentifier(namespaceLikeImport.name),
16481655
factory.createExternalModuleReference(quotedModuleSpecifier),
16491656
)
16501657
: factory.createImportDeclaration(
16511658
/*modifiers*/ undefined,
16521659
factory.createImportClause(
1653-
needsTypeOnly(namespaceLikeImport),
1660+
shouldUseTypeOnly(namespaceLikeImport, preferences),
16541661
/*name*/ undefined,
16551662
factory.createNamespaceImport(factory.createIdentifier(namespaceLikeImport.name)),
16561663
),

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8742,6 +8742,7 @@ declare namespace ts {
87428742
readonly interactiveInlayHints?: boolean;
87438743
readonly allowRenameOfImportPath?: boolean;
87448744
readonly autoImportFileExcludePatterns?: string[];
8745+
readonly preferTypeOnlyAutoImports?: boolean;
87458746
readonly organizeImportsIgnoreCase?: "auto" | boolean;
87468747
readonly organizeImportsCollation?: "ordinal" | "unicode";
87478748
readonly organizeImportsLocale?: string;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// @module: esnext
2+
// @moduleResolution: bundler
3+
4+
// @Filename: /a.ts
5+
//// export class A {}
6+
//// export class B {}
7+
8+
// @Filename: /b.ts
9+
//// let x: A/*b*/;
10+
11+
// @Filename: /c.ts
12+
//// import { A } from "./a";
13+
//// new A();
14+
//// let x: B/*c*/;
15+
16+
// @Filename: /d.ts
17+
//// new A();
18+
//// let x: B;
19+
20+
// @Filename: /ns.ts
21+
//// export * as default from "./a";
22+
23+
// @Filename: /e.ts
24+
//// let x: /*e*/ns.A;
25+
26+
goTo.marker("b");
27+
verify.importFixAtPosition([
28+
`import type { A } from "./a";
29+
30+
let x: A;`],
31+
/*errorCode*/ undefined,
32+
{
33+
preferTypeOnlyAutoImports: true,
34+
}
35+
);
36+
37+
goTo.marker("c");
38+
verify.importFixAtPosition([
39+
`import { A, type B } from "./a";
40+
new A();
41+
let x: B;`],
42+
/*errorCode*/ undefined,
43+
{
44+
preferTypeOnlyAutoImports: true,
45+
}
46+
);
47+
48+
goTo.file("/d.ts");
49+
verify.codeFixAll({
50+
fixId: "fixMissingImport",
51+
fixAllDescription: "Add all missing imports",
52+
newFileContent:
53+
`import { A, type B } from "./a";
54+
55+
new A();
56+
let x: B;`,
57+
preferences: {
58+
preferTypeOnlyAutoImports: true,
59+
},
60+
});
61+
62+
goTo.marker("e");
63+
verify.importFixAtPosition([
64+
`import type ns from "./ns";
65+
66+
let x: ns.A;`],
67+
/*errorCode*/ undefined,
68+
{
69+
preferTypeOnlyAutoImports: true,
70+
}
71+
);

tests/cases/fourslash/fourslash.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ declare namespace FourSlashInterface {
363363
docCommentTemplateAt(markerName: string | FourSlashInterface.Marker, expectedOffset: number, expectedText: string, options?: VerifyDocCommentTemplateOptions): void;
364364
noDocCommentTemplateAt(markerName: string | FourSlashInterface.Marker): void;
365365
rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void;
366-
codeFixAll(options: { fixId: string, fixAllDescription: string, newFileContent: NewFileContent, commands?: {}[] }): void;
366+
codeFixAll(options: { fixId: string, fixAllDescription: string, newFileContent: NewFileContent, commands?: {}[], preferences?: UserPreferences }): void;
367367
fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, actionName: string, formattingOptions?: FormatCodeOptions): void;
368368
rangeIs(expectedText: string, includeWhiteSpace?: boolean): void;
369369
fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void;
@@ -663,6 +663,7 @@ declare namespace FourSlashInterface {
663663
readonly providePrefixAndSuffixTextForRename?: boolean;
664664
readonly allowRenameOfImportPath?: boolean;
665665
readonly autoImportFileExcludePatterns?: readonly string[];
666+
readonly preferTypeOnlyAutoImports?: boolean;
666667
readonly organizeImportsIgnoreCase?: "auto" | boolean;
667668
readonly organizeImportsCollation?: "unicode" | "ordinal";
668669
readonly organizeImportsLocale?: string;

0 commit comments

Comments
 (0)