Repository: microsoft/TypeScript
File: src/services/codefixes/importFixes.ts (TypeScript)

Code before:
                }
                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));
        // Change .ts extension to .js if necessary
        if (!compilerOptions.allowImportingTsExtensions) {
            const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(importClause.parent);
            const resolvedModule = moduleSpecifier && getResolvedModule(sourceFile, moduleSpecifier.text, getModeForUsageLocation(sourceFile, moduleSpecifier));
            if (resolvedModule?.resolvedUsingTsExtension) {
                const changedExtension = changeAnyExtension(moduleSpecifier!.text, getOutputExtension(moduleSpecifier!.text, compilerOptions));
                changes.replaceNode(sourceFile, moduleSpecifier!, factory.createStringLiteral(changedExtension));
            }
        }
        if (convertExistingToTypeOnly) {
            const namedImports = tryCast(importClause.namedBindings, isNamedImports);
            if (namedImports && namedImports.elements.length > 1) {
                if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) &&
                    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);
                    }
                }
            }

Review comment (suggestion):
I think that there is an argument to be made here that this codefix shouldn't change the position of the promoted import. This is a separate action. I left this as-is though, as I assume that this was implemented this way for a reason.

Code after:
            const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile, preferences);
            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, preferences: UserPreferences) {
    // See comment in `doAddExistingFix` on constant with the same name.
    const convertExistingToTypeOnly = importNameElisionDisabled(compilerOptions);
    switch (aliasDeclaration.kind) {
        case SyntaxKind.ImportSpecifier:
            if (aliasDeclaration.isTypeOnly) {
                const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences);
                if (aliasDeclaration.parent.elements.length > 1 && sortKind) {
                    const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name);
                    const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive);
                    const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer);
                    if (aliasDeclaration.parent.elements.indexOf(aliasDeclaration) !== insertionIndex) {
                        changes.delete(sourceFile, aliasDeclaration);
                        changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
                        return aliasDeclaration;
                    }
                }
                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: