mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-04 21:53:42 -06:00
Add codefix and completions for promoting existing type-only imports to non-type-only (#47552)
* Import fix * Wire up completions, add sorting to fix * Fix overlapping changes when there’s only one import specifier * Update API baseline * Add sorting and filtering back to UMD fix
This commit is contained in:
parent
3718182a13
commit
0d3ff0cce8
@ -6474,6 +6474,14 @@
|
||||
"category": "Message",
|
||||
"code": 90054
|
||||
},
|
||||
"Remove 'type' from import declaration from \"{0}\"": {
|
||||
"category": "Message",
|
||||
"code": 90055
|
||||
},
|
||||
"Remove 'type' from import of '{0}' from \"{1}\"": {
|
||||
"category": "Message",
|
||||
"code": 90056
|
||||
},
|
||||
|
||||
"Convert function to an ES2015 class": {
|
||||
"category": "Message",
|
||||
|
||||
@ -3142,8 +3142,8 @@ namespace ts {
|
||||
| ImportClause & { readonly isTypeOnly: true, readonly name: Identifier }
|
||||
| ImportEqualsDeclaration & { readonly isTypeOnly: true }
|
||||
| NamespaceImport & { readonly parent: ImportClause & { readonly isTypeOnly: true } }
|
||||
| ImportSpecifier & { readonly parent: NamedImports & { readonly parent: ImportClause & { readonly isTypeOnly: true } } }
|
||||
| ExportSpecifier & { readonly parent: NamedExports & { readonly parent: ExportDeclaration & { readonly isTypeOnly: true } } }
|
||||
| ImportSpecifier & ({ readonly isTypeOnly: true } | { readonly parent: NamedImports & { readonly parent: ImportClause & { readonly isTypeOnly: true } } })
|
||||
| ExportSpecifier & ({ readonly isTypeOnly: true } | { readonly parent: NamedExports & { readonly parent: ExportDeclaration & { readonly isTypeOnly: true } } })
|
||||
;
|
||||
|
||||
/**
|
||||
|
||||
@ -10,7 +10,8 @@ namespace ts.codefix {
|
||||
Diagnostics.Cannot_find_namespace_0.code,
|
||||
Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code,
|
||||
Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here.code,
|
||||
Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.code
|
||||
Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.code,
|
||||
Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.code,
|
||||
];
|
||||
|
||||
registerCodeFix({
|
||||
@ -133,6 +134,9 @@ namespace ts.codefix {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImportFixKind.PromoteTypeOnly:
|
||||
// Excluding from fix-all
|
||||
break;
|
||||
default:
|
||||
Debug.assertNever(fix, `fix wasn't never - got kind ${(fix as ImportFix).kind}`);
|
||||
}
|
||||
@ -225,7 +229,7 @@ namespace ts.codefix {
|
||||
}
|
||||
|
||||
// Sorted with the preferred fix coming first.
|
||||
const enum ImportFixKind { UseNamespace, JsdocTypeImport, AddToExisting, AddNew }
|
||||
const enum ImportFixKind { UseNamespace, JsdocTypeImport, AddToExisting, AddNew, PromoteTypeOnly }
|
||||
// These should not be combined as bitflags, but are given powers of 2 values to
|
||||
// easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum.
|
||||
// They're also ordered in terms of increasing priority for a fix-all scenario (see
|
||||
@ -235,7 +239,8 @@ namespace ts.codefix {
|
||||
Required = 1 << 1,
|
||||
NotAllowed = 1 << 2,
|
||||
}
|
||||
type ImportFix = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport;
|
||||
type ImportFix = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport | FixPromoteTypeOnlyImport;
|
||||
type ImportFixWithModuleSpecifier = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport;
|
||||
interface FixUseNamespaceImport {
|
||||
readonly kind: ImportFixKind.UseNamespace;
|
||||
readonly namespacePrefix: string;
|
||||
@ -263,6 +268,11 @@ namespace ts.codefix {
|
||||
readonly useRequire: boolean;
|
||||
readonly exportInfo?: SymbolExportInfo;
|
||||
}
|
||||
interface FixPromoteTypeOnlyImport {
|
||||
readonly kind: ImportFixKind.PromoteTypeOnly;
|
||||
readonly typeOnlyAliasDeclaration: TypeOnlyAliasDeclaration;
|
||||
}
|
||||
|
||||
|
||||
/** Information needed to augment an existing import declaration. */
|
||||
interface FixAddToExistingImportInfo {
|
||||
@ -301,6 +311,13 @@ namespace ts.codefix {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbolToken: Identifier, program: Program, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences) {
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const symbolName = getSymbolName(sourceFile, program.getTypeChecker(), symbolToken, compilerOptions);
|
||||
const fix = getTypeOnlyPromotionFix(sourceFile, symbolToken, symbolName, program);
|
||||
return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, QuotePreference.Double, compilerOptions));
|
||||
}
|
||||
|
||||
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
|
||||
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol || info.symbol.parent === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
|
||||
const packageJsonImportFilter = createPackageJsonImportFilter(sourceFile, preferences, host);
|
||||
@ -398,7 +415,7 @@ namespace ts.codefix {
|
||||
sourceFile: SourceFile,
|
||||
host: LanguageServiceHost,
|
||||
preferences: UserPreferences,
|
||||
): readonly ImportFix[] {
|
||||
): readonly ImportFixWithModuleSpecifier[] {
|
||||
const checker = program.getTypeChecker();
|
||||
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile, program.getCompilerOptions()));
|
||||
const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker);
|
||||
@ -651,18 +668,31 @@ namespace ts.codefix {
|
||||
interface FixesInfo { readonly fixes: readonly ImportFix[]; readonly symbolName: string; }
|
||||
function getFixesInfo(context: CodeFixContextBase, errorCode: number, pos: number, useAutoImportProvider: boolean): FixesInfo | undefined {
|
||||
const symbolToken = getTokenAtPosition(context.sourceFile, pos);
|
||||
const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
|
||||
? getFixesInfoForUMDImport(context, symbolToken)
|
||||
: isIdentifier(symbolToken) ? getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider) : undefined;
|
||||
let info;
|
||||
if (errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) {
|
||||
info = getFixesInfoForUMDImport(context, symbolToken);
|
||||
}
|
||||
else if (!isIdentifier(symbolToken)) {
|
||||
return undefined;
|
||||
}
|
||||
else if (errorCode === Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.code) {
|
||||
const symbolName = getSymbolName(context.sourceFile, context.program.getTypeChecker(), symbolToken, context.program.getCompilerOptions());
|
||||
const fix = getTypeOnlyPromotionFix(context.sourceFile, symbolToken, symbolName, context.program);
|
||||
return fix && { fixes: [fix], symbolName };
|
||||
}
|
||||
else {
|
||||
info = getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider);
|
||||
}
|
||||
|
||||
const packageJsonImportFilter = createPackageJsonImportFilter(context.sourceFile, context.preferences, context.host);
|
||||
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.program, packageJsonImportFilter) };
|
||||
}
|
||||
|
||||
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter): readonly ImportFix[] {
|
||||
function sortFixes(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter): readonly ImportFixWithModuleSpecifier[] {
|
||||
return sort(fixes, (a, b) => compareValues(a.kind, b.kind) || compareModuleSpecifiers(a, b, sourceFile, program, packageJsonImportFilter.allowsImportingSpecifier));
|
||||
}
|
||||
|
||||
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter): T | undefined {
|
||||
function getBestFix<T extends ImportFixWithModuleSpecifier>(fixes: readonly T[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter): T | undefined {
|
||||
if (!some(fixes)) return;
|
||||
// These will always be placed first if available, and are better than other kinds
|
||||
if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) {
|
||||
@ -675,7 +705,7 @@ namespace ts.codefix {
|
||||
}
|
||||
|
||||
/** @returns `Comparison.LessThan` if `a` is better than `b`. */
|
||||
function compareModuleSpecifiers(a: ImportFix, b: ImportFix, importingFile: SourceFile, program: Program, allowsImportingSpecifier: (specifier: string) => boolean): Comparison {
|
||||
function compareModuleSpecifiers(a: ImportFixWithModuleSpecifier, b: ImportFixWithModuleSpecifier, importingFile: SourceFile, program: Program, allowsImportingSpecifier: (specifier: string) => boolean): Comparison {
|
||||
if (a.kind !== ImportFixKind.UseNamespace && b.kind !== ImportFixKind.UseNamespace) {
|
||||
return compareBooleans(allowsImportingSpecifier(b.moduleSpecifier), allowsImportingSpecifier(a.moduleSpecifier))
|
||||
|| compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program)
|
||||
@ -696,7 +726,7 @@ namespace ts.codefix {
|
||||
return Comparison.EqualTo;
|
||||
}
|
||||
|
||||
function getFixesInfoForUMDImport({ sourceFile, program, host, preferences }: CodeFixContextBase, token: Node): FixesInfo | undefined {
|
||||
function getFixesInfoForUMDImport({ sourceFile, program, host, preferences }: CodeFixContextBase, token: Node): FixesInfo & { fixes: readonly ImportFixWithModuleSpecifier[] } | undefined {
|
||||
const checker = program.getTypeChecker();
|
||||
const umdSymbol = getUmdSymbol(token, checker);
|
||||
if (!umdSymbol) return undefined;
|
||||
@ -765,7 +795,7 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
function getFixesInfoForNonUMDImport({ sourceFile, program, cancellationToken, host, preferences }: CodeFixContextBase, symbolToken: Identifier, useAutoImportProvider: boolean): FixesInfo | undefined {
|
||||
function getFixesInfoForNonUMDImport({ sourceFile, program, cancellationToken, host, preferences }: CodeFixContextBase, symbolToken: Identifier, useAutoImportProvider: boolean): FixesInfo & { fixes: readonly ImportFixWithModuleSpecifier[] } | undefined {
|
||||
const checker = program.getTypeChecker();
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const symbolName = getSymbolName(sourceFile, checker, symbolToken, compilerOptions);
|
||||
@ -780,6 +810,17 @@ namespace ts.codefix {
|
||||
return { fixes, symbolName };
|
||||
}
|
||||
|
||||
function getTypeOnlyPromotionFix(sourceFile: SourceFile, symbolToken: Identifier, symbolName: string, program: Program): FixPromoteTypeOnlyImport | undefined {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.resolveName(symbolName, symbolToken, SymbolFlags.Value, /*excludeGlobals*/ true);
|
||||
if (!symbol) return undefined;
|
||||
|
||||
const typeOnlyAliasDeclaration = checker.getTypeOnlyAliasDeclaration(symbol);
|
||||
if (!typeOnlyAliasDeclaration || getSourceFileOfNode(typeOnlyAliasDeclaration) !== sourceFile) return undefined;
|
||||
|
||||
return { kind: ImportFixKind.PromoteTypeOnly, typeOnlyAliasDeclaration };
|
||||
}
|
||||
|
||||
function jsxModeNeedsExplicitImport(jsx: JsxEmit | undefined) {
|
||||
return jsx === JsxEmit.React || jsx === JsxEmit.ReactNative;
|
||||
}
|
||||
@ -788,13 +829,19 @@ namespace ts.codefix {
|
||||
const parent = symbolToken.parent;
|
||||
if ((isJsxOpeningLikeElement(parent) || isJsxClosingElement(parent)) && parent.tagName === symbolToken && jsxModeNeedsExplicitImport(compilerOptions.jsx)) {
|
||||
const jsxNamespace = checker.getJsxNamespace(sourceFile);
|
||||
if (isIntrinsicJsxName(symbolToken.text) || !checker.resolveName(jsxNamespace, parent, SymbolFlags.Value, /*excludeGlobals*/ true)) {
|
||||
if (needsJsxNamespaceFix(jsxNamespace, symbolToken, checker)) {
|
||||
return jsxNamespace;
|
||||
}
|
||||
}
|
||||
return symbolToken.text;
|
||||
}
|
||||
|
||||
function needsJsxNamespaceFix(jsxNamespace: string, symbolToken: Identifier, checker: TypeChecker) {
|
||||
if (isIntrinsicJsxName(symbolToken.text)) return true; // If we were triggered by a matching error code on an intrinsic, the error must have been about missing the JSX factory
|
||||
const namespaceSymbol = checker.resolveName(jsxNamespace, symbolToken, SymbolFlags.Value, /*excludeGlobals*/ true);
|
||||
return !namespaceSymbol || some(namespaceSymbol.declarations, isTypeOnlyImportOrExportDeclaration) && !(namespaceSymbol.flags & SymbolFlags.Value);
|
||||
}
|
||||
|
||||
// Returns a map from an exported symbol's ID to a list of every way it's (re-)exported.
|
||||
function getExportInfos(
|
||||
symbolName: string,
|
||||
@ -911,11 +958,83 @@ namespace ts.codefix {
|
||||
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true);
|
||||
return [importKind === ImportKind.Default ? Diagnostics.Import_default_0_from_module_1 : Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier];
|
||||
}
|
||||
case ImportFixKind.PromoteTypeOnly: {
|
||||
const { typeOnlyAliasDeclaration } = fix;
|
||||
const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile);
|
||||
return promotedDeclaration.kind === SyntaxKind.ImportSpecifier
|
||||
? [Diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)]
|
||||
: [Diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)];
|
||||
}
|
||||
default:
|
||||
return Debug.assertNever(fix, `Unexpected fix kind ${(fix as ImportFix).kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getModuleSpecifierText(promotedDeclaration: ImportClause | ImportEqualsDeclaration): string {
|
||||
return promotedDeclaration.kind === SyntaxKind.ImportEqualsDeclaration
|
||||
? tryCast(tryCast(promotedDeclaration.moduleReference, isExternalModuleReference)?.expression, isStringLiteralLike)?.text || promotedDeclaration.moduleReference.getText()
|
||||
: cast(promotedDeclaration.parent.moduleSpecifier, isStringLiteral).text;
|
||||
}
|
||||
|
||||
function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile) {
|
||||
// See comment in `doAddExistingFix` on constant with the same name.
|
||||
const convertExistingToTypeOnly = compilerOptions.preserveValueImports && compilerOptions.isolatedModules;
|
||||
switch (aliasDeclaration.kind) {
|
||||
case SyntaxKind.ImportSpecifier:
|
||||
if (aliasDeclaration.isTypeOnly) {
|
||||
if (aliasDeclaration.parent.elements.length > 1 && OrganizeImports.importSpecifiersAreSorted(aliasDeclaration.parent.elements)) {
|
||||
changes.delete(sourceFile, aliasDeclaration);
|
||||
const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name);
|
||||
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier);
|
||||
changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
|
||||
}
|
||||
else {
|
||||
changes.deleteRange(sourceFile, aliasDeclaration.getFirstToken()!);
|
||||
}
|
||||
return aliasDeclaration;
|
||||
}
|
||||
else {
|
||||
Debug.assert(aliasDeclaration.parent.parent.isTypeOnly);
|
||||
promoteImportClause(aliasDeclaration.parent.parent);
|
||||
return aliasDeclaration.parent.parent;
|
||||
}
|
||||
case SyntaxKind.ImportClause:
|
||||
promoteImportClause(aliasDeclaration);
|
||||
return aliasDeclaration;
|
||||
case SyntaxKind.NamespaceImport:
|
||||
promoteImportClause(aliasDeclaration.parent);
|
||||
return aliasDeclaration.parent;
|
||||
case SyntaxKind.ImportEqualsDeclaration:
|
||||
changes.deleteRange(sourceFile, aliasDeclaration.getChildAt(1));
|
||||
return aliasDeclaration;
|
||||
default:
|
||||
Debug.failBadSyntaxKind(aliasDeclaration);
|
||||
}
|
||||
|
||||
function promoteImportClause(importClause: ImportClause) {
|
||||
changes.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(importClause, sourceFile));
|
||||
if (convertExistingToTypeOnly) {
|
||||
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
|
||||
if (namedImports && namedImports.elements.length > 1) {
|
||||
if (OrganizeImports.importSpecifiersAreSorted(namedImports.elements) &&
|
||||
aliasDeclaration.kind === SyntaxKind.ImportSpecifier &&
|
||||
namedImports.elements.indexOf(aliasDeclaration) !== 0
|
||||
) {
|
||||
// The import specifier being promoted will be the only non-type-only,
|
||||
// import in the NamedImports, so it should be moved to the front.
|
||||
changes.delete(sourceFile, aliasDeclaration);
|
||||
changes.insertImportSpecifierAtIndex(sourceFile, aliasDeclaration, namedImports, 0);
|
||||
}
|
||||
for (const element of namedImports.elements) {
|
||||
if (element !== aliasDeclaration && !element.isTypeOnly) {
|
||||
changes.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doAddExistingFix(
|
||||
changes: textChanges.ChangeTracker,
|
||||
sourceFile: SourceFile,
|
||||
@ -965,17 +1084,7 @@ namespace ts.codefix {
|
||||
const insertionIndex = convertExistingToTypeOnly && !spec.isTypeOnly
|
||||
? 0
|
||||
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec);
|
||||
const prevSpecifier = (clause.namedBindings as NamedImports).elements[insertionIndex - 1];
|
||||
if (prevSpecifier) {
|
||||
changes.insertNodeInListAfter(sourceFile, prevSpecifier, spec);
|
||||
}
|
||||
else {
|
||||
changes.insertNodeBefore(
|
||||
sourceFile,
|
||||
existingSpecifiers[0],
|
||||
spec,
|
||||
!positionsAreOnSameLine(existingSpecifiers[0].getStart(), clause.parent.getStart(), sourceFile));
|
||||
}
|
||||
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
|
||||
}
|
||||
}
|
||||
else if (existingSpecifiers?.length) {
|
||||
|
||||
@ -60,6 +60,8 @@ namespace ts.Completions {
|
||||
ThisProperty = "ThisProperty/",
|
||||
/** Auto-import that comes attached to a class member snippet */
|
||||
ClassMemberSnippet = "ClassMemberSnippet/",
|
||||
/** A type-only import that needs to be promoted in order to be used at the completion location */
|
||||
TypeOnlyAlias = "TypeOnlyAlias/",
|
||||
}
|
||||
|
||||
const enum SymbolOriginInfoKind {
|
||||
@ -69,6 +71,7 @@ namespace ts.Completions {
|
||||
Promise = 1 << 3,
|
||||
Nullable = 1 << 4,
|
||||
ResolvedExport = 1 << 5,
|
||||
TypeOnlyAlias = 1 << 6,
|
||||
|
||||
SymbolMemberNoExport = SymbolMember,
|
||||
SymbolMemberExport = SymbolMember | Export,
|
||||
@ -96,6 +99,10 @@ namespace ts.Completions {
|
||||
moduleSpecifier: string;
|
||||
}
|
||||
|
||||
interface SymbolOriginInfoTypeOnlyAlias extends SymbolOriginInfo {
|
||||
declaration: TypeOnlyAliasDeclaration;
|
||||
}
|
||||
|
||||
function originIsThisType(origin: SymbolOriginInfo): boolean {
|
||||
return !!(origin.kind & SymbolOriginInfoKind.ThisType);
|
||||
}
|
||||
@ -128,6 +135,10 @@ namespace ts.Completions {
|
||||
return !!(origin.kind & SymbolOriginInfoKind.Nullable);
|
||||
}
|
||||
|
||||
function originIsTypeOnlyAlias(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoTypeOnlyAlias {
|
||||
return !!(origin && origin.kind & SymbolOriginInfoKind.TypeOnlyAlias);
|
||||
}
|
||||
|
||||
interface UniqueNameSet {
|
||||
add(name: string): void;
|
||||
has(name: string): boolean;
|
||||
@ -740,6 +751,10 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
|
||||
if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) {
|
||||
hasAction = true;
|
||||
}
|
||||
|
||||
if (preferences.includeCompletionsWithClassMemberSnippets &&
|
||||
preferences.includeCompletionsWithInsertText &&
|
||||
completionKind === CompletionKind.MemberLike &&
|
||||
@ -1168,6 +1183,9 @@ namespace ts.Completions {
|
||||
if (origin?.kind === SymbolOriginInfoKind.ThisType) {
|
||||
return CompletionSource.ThisProperty;
|
||||
}
|
||||
if (origin?.kind === SymbolOriginInfoKind.TypeOnlyAlias) {
|
||||
return CompletionSource.TypeOnlyAlias;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCompletionEntriesFromSymbols(
|
||||
@ -1245,7 +1263,7 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
/** True for locals; false for globals, module exports from other files, `this.` completions. */
|
||||
const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile()));
|
||||
const shouldShadowLaterSymbols = (!origin || originIsTypeOnlyAlias(origin)) && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile()));
|
||||
uniques.set(name, shouldShadowLaterSymbols);
|
||||
insertSorted(entries, entry, compareCompletionEntries, /*allowDuplicates*/ true);
|
||||
}
|
||||
@ -1261,6 +1279,7 @@ namespace ts.Completions {
|
||||
};
|
||||
|
||||
function shouldIncludeSymbol(symbol: Symbol, symbolToSortTextIdMap: SymbolSortTextIdMap): boolean {
|
||||
let allFlags = symbol.flags;
|
||||
if (!isSourceFile(location)) {
|
||||
// export = /**/ here we want to get all meanings, so any symbol is ok
|
||||
if (isExportAssignment(location.parent)) {
|
||||
@ -1287,12 +1306,12 @@ namespace ts.Completions {
|
||||
|| symbolToSortTextIdMap[getSymbolId(symbolOrigin)] === SortTextId.LocationPriority)) {
|
||||
return false;
|
||||
}
|
||||
// Continue with origin symbol
|
||||
symbol = symbolOrigin;
|
||||
|
||||
allFlags |= getCombinedLocalAndExportSymbolFlags(symbolOrigin);
|
||||
|
||||
// import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace)
|
||||
if (isInRightSideOfInternalImportEqualsDeclaration(location)) {
|
||||
return !!(symbol.flags & SymbolFlags.Namespace);
|
||||
return !!(allFlags & SymbolFlags.Namespace);
|
||||
}
|
||||
|
||||
if (isTypeOnlyLocation) {
|
||||
@ -1302,7 +1321,7 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
// expressions are value space (which includes the value namespaces)
|
||||
return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value);
|
||||
return !!(allFlags & SymbolFlags.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1533,6 +1552,19 @@ namespace ts.Completions {
|
||||
}
|
||||
}
|
||||
|
||||
if (originIsTypeOnlyAlias(origin)) {
|
||||
const codeAction = codefix.getPromoteTypeOnlyCompletionAction(
|
||||
sourceFile,
|
||||
origin.declaration.name,
|
||||
program,
|
||||
host,
|
||||
formatContext,
|
||||
preferences);
|
||||
|
||||
Debug.assertIsDefined(codeAction, "Expected to have a code action for promoting type-only alias");
|
||||
return { codeActions: [codeAction], sourceDisplay: undefined };
|
||||
}
|
||||
|
||||
if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) {
|
||||
return { codeActions: undefined, sourceDisplay: undefined };
|
||||
}
|
||||
@ -2314,14 +2346,23 @@ namespace ts.Completions {
|
||||
isInSnippetScope = isSnippetScope(scopeNode);
|
||||
|
||||
const symbolMeanings = (isTypeOnlyLocation ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias;
|
||||
const typeOnlyAliasNeedsPromotion = previousToken && !isValidTypeOnlyAliasUseSite(previousToken);
|
||||
|
||||
symbols = concatenate(symbols, typeChecker.getSymbolsInScope(scopeNode, symbolMeanings));
|
||||
Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined");
|
||||
for (const symbol of symbols) {
|
||||
for (let i = 0; i < symbols.length; i++) {
|
||||
const symbol = symbols[i];
|
||||
if (!typeChecker.isArgumentsSymbol(symbol) &&
|
||||
!some(symbol.declarations, d => d.getSourceFile() === sourceFile)) {
|
||||
symbolToSortTextIdMap[getSymbolId(symbol)] = SortTextId.GlobalsOrKeywords;
|
||||
}
|
||||
if (typeOnlyAliasNeedsPromotion && !(symbol.flags & SymbolFlags.Value)) {
|
||||
const typeOnlyAliasDeclaration = symbol.declarations && find(symbol.declarations, isTypeOnlyImportOrExportDeclaration);
|
||||
if (typeOnlyAliasDeclaration) {
|
||||
const origin: SymbolOriginInfoTypeOnlyAlias = { kind: SymbolOriginInfoKind.TypeOnlyAlias, declaration: typeOnlyAliasDeclaration };
|
||||
symbolToOriginInfoMap[i] = origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
|
||||
@ -3905,10 +3946,15 @@ namespace ts.Completions {
|
||||
|
||||
/** True if symbol is a type or a module containing at least one type. */
|
||||
function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, checker: TypeChecker, seenModules = new Map<SymbolId, true>()): boolean {
|
||||
const sym = skipAlias(symbol.exportSymbol || symbol, checker);
|
||||
return !!(sym.flags & SymbolFlags.Type) || checker.isUnknownSymbol(sym) ||
|
||||
!!(sym.flags & SymbolFlags.Module) && addToSeen(seenModules, getSymbolId(sym)) &&
|
||||
checker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, checker, seenModules));
|
||||
// Since an alias can be merged with a local declaration, we need to test both the alias and its target.
|
||||
// This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings.
|
||||
return nonAliasCanBeReferencedAtTypeLocation(symbol) || nonAliasCanBeReferencedAtTypeLocation(skipAlias(symbol.exportSymbol || symbol, checker));
|
||||
|
||||
function nonAliasCanBeReferencedAtTypeLocation(symbol: Symbol): boolean {
|
||||
return !!(symbol.flags & SymbolFlags.Type) || checker.isUnknownSymbol(symbol) ||
|
||||
!!(symbol.flags & SymbolFlags.Module) && addToSeen(seenModules, getSymbolId(symbol)) &&
|
||||
checker.getExportsOfModule(symbol).some(e => symbolCanBeReferencedAtTypeLocation(e, checker, seenModules));
|
||||
}
|
||||
}
|
||||
|
||||
function isDeprecated(symbol: Symbol, checker: TypeChecker) {
|
||||
|
||||
@ -339,6 +339,7 @@ namespace ts.textChanges {
|
||||
this.deletedNodes.push({ sourceFile, node });
|
||||
}
|
||||
|
||||
/** Stop! Consider using `delete` instead, which has logic for deleting nodes from delimited lists. */
|
||||
public deleteNode(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
|
||||
this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options));
|
||||
}
|
||||
@ -786,6 +787,20 @@ namespace ts.textChanges {
|
||||
this.insertText(sourceFile, node.getStart(sourceFile), "export ");
|
||||
}
|
||||
|
||||
public insertImportSpecifierAtIndex(sourceFile: SourceFile, importSpecifier: ImportSpecifier, namedImports: NamedImports, index: number) {
|
||||
const prevSpecifier = namedImports.elements[index - 1];
|
||||
if (prevSpecifier) {
|
||||
this.insertNodeInListAfter(sourceFile, prevSpecifier, importSpecifier);
|
||||
}
|
||||
else {
|
||||
this.insertNodeBefore(
|
||||
sourceFile,
|
||||
namedImports.elements[0],
|
||||
importSpecifier,
|
||||
!positionsAreOnSameLine(namedImports.elements[0].getStart(), namedImports.parent.parent.getStart(), sourceFile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
|
||||
* i.e. arguments in arguments lists, parameters in parameter lists etc.
|
||||
|
||||
@ -1720,19 +1720,23 @@ declare namespace ts {
|
||||
readonly parent: ImportClause & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
} | ImportSpecifier & {
|
||||
} | ImportSpecifier & ({
|
||||
readonly isTypeOnly: true;
|
||||
} | {
|
||||
readonly parent: NamedImports & {
|
||||
readonly parent: ImportClause & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
};
|
||||
} | ExportSpecifier & {
|
||||
}) | ExportSpecifier & ({
|
||||
readonly isTypeOnly: true;
|
||||
} | {
|
||||
readonly parent: NamedExports & {
|
||||
readonly parent: ExportDeclaration & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
/**
|
||||
* This is either an `export =` or an `export default` declaration.
|
||||
* Unless `isExportEquals` is set, this node was parsed as an `export default`.
|
||||
|
||||
10
tests/baselines/reference/api/typescript.d.ts
vendored
10
tests/baselines/reference/api/typescript.d.ts
vendored
@ -1720,19 +1720,23 @@ declare namespace ts {
|
||||
readonly parent: ImportClause & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
} | ImportSpecifier & {
|
||||
} | ImportSpecifier & ({
|
||||
readonly isTypeOnly: true;
|
||||
} | {
|
||||
readonly parent: NamedImports & {
|
||||
readonly parent: ImportClause & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
};
|
||||
} | ExportSpecifier & {
|
||||
}) | ExportSpecifier & ({
|
||||
readonly isTypeOnly: true;
|
||||
} | {
|
||||
readonly parent: NamedExports & {
|
||||
readonly parent: ExportDeclaration & {
|
||||
readonly isTypeOnly: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
/**
|
||||
* This is either an `export =` or an `export default` declaration.
|
||||
* Unless `isExportEquals` is set, this node was parsed as an `export default`.
|
||||
|
||||
34
tests/cases/fourslash/completionsImport_promoteTypeOnly1.ts
Normal file
34
tests/cases/fourslash/completionsImport_promoteTypeOnly1.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
// @module: es2015
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import type { SomeInterface, SomePig } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
verify.completions({
|
||||
marker: "",
|
||||
exact: completion.globalsPlus([{
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
hasAction: true,
|
||||
}]),
|
||||
preferences: { includeCompletionsForModuleExports: true },
|
||||
});
|
||||
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
description: `Remove 'type' from import declaration from "./exports.js"`,
|
||||
newFileContent:
|
||||
`import { SomeInterface, SomePig } from "./exports.js";
|
||||
new SomePig`,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
allowIncompleteCompletions: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
19
tests/cases/fourslash/completionsImport_promoteTypeOnly2.ts
Normal file
19
tests/cases/fourslash/completionsImport_promoteTypeOnly2.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @module: es2015
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import type { SomeInterface } from "./exports.js";
|
||||
//// const SomeInterface = {};
|
||||
//// SomeI/**/
|
||||
|
||||
// Should NOT promote this
|
||||
verify.completions({
|
||||
marker: "",
|
||||
includes: [{
|
||||
name: "SomeInterface"
|
||||
}]
|
||||
});
|
||||
33
tests/cases/fourslash/completionsImport_promoteTypeOnly3.ts
Normal file
33
tests/cases/fourslash/completionsImport_promoteTypeOnly3.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
// @module: es2015
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import { type SomePig } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
verify.completions({
|
||||
marker: "",
|
||||
includes: [{
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
hasAction: true,
|
||||
}]
|
||||
});
|
||||
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
description: `Remove 'type' from import of 'SomePig' from "./exports.js"`,
|
||||
newFileContent:
|
||||
`import { SomePig } from "./exports.js";
|
||||
new SomePig`,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
allowIncompleteCompletions: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
35
tests/cases/fourslash/completionsImport_promoteTypeOnly4.ts
Normal file
35
tests/cases/fourslash/completionsImport_promoteTypeOnly4.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
// @module: es2015
|
||||
// @isolatedModules: true
|
||||
// @preserveValueImports: true
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import type { SomePig, SomeInterface } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
verify.completions({
|
||||
marker: "",
|
||||
includes: [{
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
hasAction: true,
|
||||
}]
|
||||
});
|
||||
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "SomePig",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
description: `Remove 'type' from import declaration from "./exports.js"`,
|
||||
newFileContent:
|
||||
`import { SomePig, type SomeInterface } from "./exports.js";
|
||||
new SomePig`,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
allowIncompleteCompletions: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
33
tests/cases/fourslash/completionsImport_promoteTypeOnly5.ts
Normal file
33
tests/cases/fourslash/completionsImport_promoteTypeOnly5.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
// @module: es2015
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import { type SomePig as Babe } from "./exports.js";
|
||||
//// new Babe/**/
|
||||
|
||||
verify.completions({
|
||||
marker: "",
|
||||
includes: [{
|
||||
name: "Babe",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
hasAction: true,
|
||||
}]
|
||||
});
|
||||
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "Babe",
|
||||
source: completion.CompletionSource.TypeOnlyAlias,
|
||||
description: `Remove 'type' from import of 'Babe' from "./exports.js"`,
|
||||
newFileContent:
|
||||
`import { SomePig as Babe } from "./exports.js";
|
||||
new Babe`,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
allowIncompleteCompletions: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
@ -858,6 +858,7 @@ declare namespace completion {
|
||||
export const enum CompletionSource {
|
||||
ThisProperty = "ThisProperty/",
|
||||
ClassMemberSnippet = "ClassMemberSnippet/",
|
||||
TypeOnlyAlias = "TypeOnlyAlias/",
|
||||
}
|
||||
export const globalThisEntry: Entry;
|
||||
export const undefinedVarEntry: Entry;
|
||||
|
||||
16
tests/cases/fourslash/importNameCodeFix_importType5.ts
Normal file
16
tests/cases/fourslash/importNameCodeFix_importType5.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @module: es2015
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import type { SomeInterface, SomePig } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
goTo.marker("");
|
||||
verify.importFixAtPosition([
|
||||
`import { SomeInterface, SomePig } from "./exports.js";
|
||||
new SomePig`]);
|
||||
19
tests/cases/fourslash/importNameCodeFix_importType6.ts
Normal file
19
tests/cases/fourslash/importNameCodeFix_importType6.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
// @module: es2015
|
||||
// @esModuleInterop: true
|
||||
// @jsx: react
|
||||
|
||||
// @Filename: /types.d.ts
|
||||
//// declare module "react" { var React: any; export = React; export as namespace React; }
|
||||
|
||||
// @Filename: /a.tsx
|
||||
//// import type React from "react";
|
||||
//// function Component() {}
|
||||
//// (<Component/**/ />)
|
||||
|
||||
goTo.marker("");
|
||||
|
||||
verify.importFixAtPosition([
|
||||
`import React from "react";
|
||||
function Component() {}
|
||||
(<Component />)`]);
|
||||
24
tests/cases/fourslash/importNameCodeFix_importType7.ts
Normal file
24
tests/cases/fourslash/importNameCodeFix_importType7.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @module: es2015
|
||||
|
||||
// sorting and multiline imports and trailing commas, oh my
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import {
|
||||
//// type SomeInterface,
|
||||
//// type SomePig,
|
||||
//// } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
goTo.marker("");
|
||||
verify.importFixAtPosition([
|
||||
`import {
|
||||
SomePig,
|
||||
type SomeInterface,
|
||||
} from "./exports.js";
|
||||
new SomePig`]);
|
||||
18
tests/cases/fourslash/importNameCodeFix_importType8.ts
Normal file
18
tests/cases/fourslash/importNameCodeFix_importType8.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @module: es2015
|
||||
// @isolatedModules: true
|
||||
// @preserveValueImports: true
|
||||
|
||||
// @Filename: /exports.ts
|
||||
//// export interface SomeInterface {}
|
||||
//// export class SomePig {}
|
||||
|
||||
// @Filename: /a.ts
|
||||
//// import type { SomeInterface, SomePig } from "./exports.js";
|
||||
//// new SomePig/**/
|
||||
|
||||
goTo.marker("");
|
||||
verify.importFixAtPosition([
|
||||
`import { SomePig, type SomeInterface } from "./exports.js";
|
||||
new SomePig`]);
|
||||
Loading…
x
Reference in New Issue
Block a user