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:
Andrew Branch 2022-01-26 15:07:41 -08:00 committed by GitHub
parent 3718182a13
commit 0d3ff0cce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 460 additions and 42 deletions

View File

@ -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",

View File

@ -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 } } })
;
/**

View File

@ -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) {

View File

@ -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) {

View File

@ -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.

View File

@ -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`.

View File

@ -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`.

View 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,
},
});

View 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"
}]
});

View 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,
},
});

View 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,
},
});

View 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,
},
});

View File

@ -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;

View 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`]);

View 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 />)`]);

View 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`]);

View 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`]);