Skip to content

Commit e1bce18

Browse files
authored
Type-only auto imports (#36412)
* WIP * Promote existing type-only imports to regular if needed * Add completions test adding to type-only import * Update tests, revert whole-import-clause replacement codefix strategy to preserve import specifier formatting * Revert unnecessary changes * Delete unused function * }
1 parent 757e670 commit e1bce18

13 files changed

+106
-84
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,12 +1879,7 @@ namespace ts {
18791879
}
18801880

18811881
function checkSymbolUsageInExpressionContext(symbol: Symbol, name: __String, useSite: Node) {
1882-
if (
1883-
!(useSite.flags & NodeFlags.Ambient) &&
1884-
!isPartOfTypeQuery(useSite) &&
1885-
!isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite) &&
1886-
isExpressionNode(useSite)
1887-
) {
1882+
if (!isValidTypeOnlyAliasUseSite(useSite)) {
18881883
const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(symbol);
18891884
if (typeOnlyDeclaration) {
18901885
const message = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier

src/compiler/utilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6134,4 +6134,11 @@ namespace ts {
61346134
export function pseudoBigIntToString({negative, base10Value}: PseudoBigInt): string {
61356135
return (negative && base10Value !== "0" ? "-" : "") + base10Value;
61366136
}
6137+
6138+
export function isValidTypeOnlyAliasUseSite(useSite: Node): boolean {
6139+
return !!(useSite.flags & NodeFlags.Ambient)
6140+
|| isPartOfTypeQuery(useSite)
6141+
|| isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite)
6142+
|| !isExpressionNode(useSite);
6143+
}
61376144
}

src/services/codefixes/importFixes.ts

Lines changed: 38 additions & 53 deletions
Large diffs are not rendered by default.

src/services/completions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ namespace ts.Completions {
525525
symbolToOriginInfoMap: SymbolOriginInfoMap;
526526
previousToken: Node | undefined;
527527
readonly isJsxInitializer: IsJsxInitializer;
528+
readonly isTypeOnlyLocation: boolean;
528529
}
529530
function getSymbolCompletionFromEntryId(
530531
program: Program,
@@ -543,7 +544,7 @@ namespace ts.Completions {
543544
return { type: "request", request: completionData };
544545
}
545546

546-
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
547+
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;
547548

548549
const literal = find(literals, l => completionNameForLiteral(l) === entryId.name);
549550
if (literal !== undefined) return { type: "literal", literal };
@@ -556,7 +557,7 @@ namespace ts.Completions {
556557
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
557558
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind);
558559
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
559-
? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
560+
? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation }
560561
: undefined;
561562
}) || { type: "none" };
562563
}
@@ -714,6 +715,7 @@ namespace ts.Completions {
714715
readonly isJsxInitializer: IsJsxInitializer;
715716
readonly insideJsDocTagTypeExpression: boolean;
716717
readonly symbolToSortTextMap: SymbolSortTextMap;
718+
readonly isTypeOnlyLocation: boolean;
717719
}
718720
type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag };
719721

@@ -1012,6 +1014,7 @@ namespace ts.Completions {
10121014
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
10131015
const symbolToSortTextMap: SymbolSortTextMap = [];
10141016
const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache();
1017+
const isTypeOnly = isTypeOnlyCompletion();
10151018

10161019
if (isRightOfDot || isRightOfQuestionDot) {
10171020
getTypeScriptMemberSymbols();
@@ -1061,7 +1064,8 @@ namespace ts.Completions {
10611064
previousToken,
10621065
isJsxInitializer,
10631066
insideJsDocTagTypeExpression,
1064-
symbolToSortTextMap
1067+
symbolToSortTextMap,
1068+
isTypeOnlyLocation: isTypeOnly
10651069
};
10661070

10671071
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@@ -1329,7 +1333,6 @@ namespace ts.Completions {
13291333
const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile;
13301334
isInSnippetScope = isSnippetScope(scopeNode);
13311335

1332-
const isTypeOnly = isTypeOnlyCompletion();
13331336
const symbolMeanings = (isTypeOnly ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias;
13341337

13351338
symbols = Debug.assertEachDefined(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings), "getSymbolsInScope() should all be defined");

src/services/utilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,10 @@ namespace ts {
13061306
return contains(typeKeywords, kind);
13071307
}
13081308

1309+
export function isTypeKeywordToken(node: Node): node is Token<SyntaxKind.TypeKeyword> {
1310+
return node.kind === SyntaxKind.TypeKeyword;
1311+
}
1312+
13091313
/** True if the symbol is for an external module, as opposed to a namespace. */
13101314
export function isExternalModuleSymbol(moduleSymbol: Symbol): boolean {
13111315
return !!(moduleSymbol.flags & SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
@@ -1494,6 +1498,11 @@ namespace ts {
14941498
}
14951499
}
14961500

1501+
export function getTypeKeywordOfTypeOnlyImport(importClause: ImportClause, sourceFile: SourceFile): Token<SyntaxKind.TypeKeyword> {
1502+
Debug.assert(importClause.isTypeOnly);
1503+
return cast(importClause.getChildAt(0, sourceFile), isTypeKeywordToken);
1504+
}
1505+
14971506
export function textSpansEqual(a: TextSpan | undefined, b: TextSpan | undefined): boolean {
14981507
return !!a && !!b && a.start === b.start && a.length === b.length;
14991508
}

tests/cases/fourslash/codeFixInferFromUsageContextualImport2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ goTo.file("/b.ts");
2020
verify.codeFix({
2121
description: "Infer parameter types from usage",
2222
newFileContent:
23-
`import type { User } from "./a";
23+
`import { User } from "./a";
2424
2525
export function f(user: User) {
2626
getEmail(user);

tests/cases/fourslash/completionsImport_exportEquals.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ verify.applyCodeActionFromCompletion("1", {
4343
source: "/a",
4444
description: `Import 'b' from module "./a"`,
4545
newFileContent:
46-
`import type { b } from "./a";
46+
`import { b } from "./a";
4747
4848
a;
4949
let x: b;`,
@@ -54,7 +54,7 @@ verify.applyCodeActionFromCompletion("0", {
5454
source: "/a",
5555
description: `Import 'a' from module "./a"`,
5656
newFileContent:
57-
`import type { b } from "./a";
57+
`import { b } from "./a";
5858
import a = require("./a");
5959
6060
a;
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+
// @target: esnext
4+
5+
// @Filename: /a.ts
6+
//// export class A {}
7+
//// export class B {}
8+
9+
// @Filename: /b.ts
10+
//// import type { A } from './a';
11+
//// const b: B/**/
12+
13+
goTo.file('/b.ts');
14+
verify.applyCodeActionFromCompletion('', {
15+
name: 'B',
16+
source: '/a',
17+
description: `Add 'B' to existing import declaration from "./a"`,
18+
preferences: {
19+
includeCompletionsForModuleExports: true,
20+
includeInsertTextCompletions: true
21+
},
22+
newFileContent: `import type { A, B } from './a';
23+
const b: B`
24+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /a.ts
4+
//// export class A {}
5+
//// export class B {}
6+
7+
// @Filename: /b.ts
8+
//// import type { A } from './a';
9+
//// new B
10+
11+
goTo.file('/b.ts');
12+
verify.importFixAtPosition([`import { A, B } from './a';
13+
new B`]);

tests/cases/fourslash/importNameCodeFixNewImportFile3.ts

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

1111
verify.importFixAtPosition([
12-
`import type { XXX } from "./module";
12+
`import { XXX } from "./module";
1313
1414
let t: XXX.I;`
1515
]);

tests/cases/fourslash/importNameCodeFixNewImportFile4.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//// }
1111

1212
verify.importFixAtPosition([
13-
`import type { A } from "./module";
13+
`import { A } from "./module";
1414
1515
let t: A.B.I;`
1616
]);

tests/cases/fourslash/importNameCodeFixNewImportTypeOnly.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

tests/cases/fourslash/importNameCodeFix_exportEquals.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ verify.codeFixAll({
1616
fixId: "fixMissingImport",
1717
fixAllDescription: "Add all missing imports",
1818
newFileContent:
19-
`import a = require("./a");
19+
`import { b } from "./a";
2020
21-
import type { b } from "./a";
21+
import a = require("./a");
2222
2323
a;
2424
let x: b;`,

0 commit comments

Comments
 (0)