Skip to content

Commit 1bcf069

Browse files
Copilotandrewbranch
andcommitted
Convert UMD and type-only fixes to use autoimport.Fix
- Add TypeOnlyAliasDeclaration field to autoimport.Fix - Implement PromoteTypeOnly case in Fix.Edits() - Move promoteFromTypeOnly and helper functions to autoimport package - Convert UMD fixes from old ImportFix to new autoimport.Fix via converter - Remove oldFix field from fixInfo struct - now all fixes use autoimport.Fix - Simplify getImportCodeActions to only handle autoimport.Fix type Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
1 parent 7f2d093 commit 1bcf069

File tree

2 files changed

+192
-5
lines changed

2 files changed

+192
-5
lines changed

internal/ls/autoimport/fix.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/microsoft/typescript-go/internal/ast"
1111
"github.com/microsoft/typescript-go/internal/astnav"
12+
"github.com/microsoft/typescript-go/internal/checker"
1213
"github.com/microsoft/typescript-go/internal/collections"
1314
"github.com/microsoft/typescript-go/internal/compiler"
1415
"github.com/microsoft/typescript-go/internal/core"
@@ -21,6 +22,7 @@ import (
2122
"github.com/microsoft/typescript-go/internal/ls/organizeimports"
2223
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
2324
"github.com/microsoft/typescript-go/internal/modulespecifiers"
25+
"github.com/microsoft/typescript-go/internal/scanner"
2426
"github.com/microsoft/typescript-go/internal/stringutil"
2527
"github.com/microsoft/typescript-go/internal/tspath"
2628
)
@@ -106,6 +108,14 @@ func (f *Fix) Edits(
106108
// addNamespaceQualifier(tracker, file, qualification)
107109
// }
108110
return tracker.GetChanges()[file.FileName()], diagnostics.Add_import_from_0.Format(f.ModuleSpecifier)
111+
case lsproto.AutoImportFixKindPromoteTypeOnly:
112+
promotedDeclaration := promoteFromTypeOnly(tracker, f.TypeOnlyAliasDeclaration, compilerOptions, file, preferences)
113+
if promotedDeclaration.Kind == ast.KindImportSpecifier {
114+
moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent)
115+
return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_of_0_from_1.Format(f.Name, moduleSpec)
116+
}
117+
moduleSpec := getModuleSpecifierText(promotedDeclaration)
118+
return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_declaration_from_0.Format(moduleSpec)
109119
default:
110120
panic("unimplemented fix edit")
111121
}
@@ -712,6 +722,186 @@ func isIndexFileName(fileName string) bool {
712722
return fileName == "index"
713723
}
714724

725+
func promoteFromTypeOnly(
726+
changes *change.Tracker,
727+
aliasDeclaration *ast.Declaration,
728+
compilerOptions *core.CompilerOptions,
729+
sourceFile *ast.SourceFile,
730+
preferences *lsutil.UserPreferences,
731+
) *ast.Declaration {
732+
// See comment in `doAddExistingFix` on constant with the same name.
733+
convertExistingToTypeOnly := compilerOptions.VerbatimModuleSyntax
734+
735+
switch aliasDeclaration.Kind {
736+
case ast.KindImportSpecifier:
737+
spec := aliasDeclaration.AsImportSpecifier()
738+
if spec.IsTypeOnly {
739+
if spec.Parent != nil && spec.Parent.Kind == ast.KindNamedImports {
740+
// TypeScript creates a new specifier with isTypeOnly=false, computes insertion index,
741+
// and if different from current position, deletes and re-inserts at new position.
742+
// For now, we just delete the range from the first token (type keyword) to the property name or name.
743+
firstToken := lsutil.GetFirstToken(aliasDeclaration, sourceFile)
744+
typeKeywordPos := scanner.GetTokenPosOfNode(firstToken, sourceFile, false)
745+
var targetNode *ast.DeclarationName
746+
if spec.PropertyName != nil {
747+
targetNode = spec.PropertyName
748+
} else {
749+
targetNode = spec.Name()
750+
}
751+
targetPos := scanner.GetTokenPosOfNode(targetNode.AsNode(), sourceFile, false)
752+
changes.DeleteRange(sourceFile, core.NewTextRange(typeKeywordPos, targetPos))
753+
}
754+
return aliasDeclaration
755+
} else {
756+
// The parent import clause is type-only
757+
if spec.Parent == nil || spec.Parent.Kind != ast.KindNamedImports {
758+
panic("ImportSpecifier parent must be NamedImports")
759+
}
760+
if spec.Parent.Parent == nil || spec.Parent.Parent.Kind != ast.KindImportClause {
761+
panic("NamedImports parent must be ImportClause")
762+
}
763+
promoteImportClause(changes, spec.Parent.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration)
764+
return spec.Parent.Parent
765+
}
766+
767+
case ast.KindImportClause:
768+
promoteImportClause(changes, aliasDeclaration.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration)
769+
return aliasDeclaration
770+
771+
case ast.KindNamespaceImport:
772+
// Promote the parent import clause
773+
if aliasDeclaration.Parent == nil || aliasDeclaration.Parent.Kind != ast.KindImportClause {
774+
panic("NamespaceImport parent must be ImportClause")
775+
}
776+
promoteImportClause(changes, aliasDeclaration.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration)
777+
return aliasDeclaration.Parent
778+
779+
case ast.KindImportEqualsDeclaration:
780+
// Remove the 'type' keyword (which is the second token: 'import' 'type' name '=' ...)
781+
importEqDecl := aliasDeclaration.AsImportEqualsDeclaration()
782+
// The type keyword is after 'import' and before the name
783+
scan := scanner.GetScannerForSourceFile(sourceFile, importEqDecl.Pos())
784+
// Skip 'import' keyword to get to 'type'
785+
scan.Scan()
786+
deleteTypeKeyword(changes, sourceFile, scan.TokenStart())
787+
return aliasDeclaration
788+
default:
789+
panic(fmt.Sprintf("Unexpected alias declaration kind: %v", aliasDeclaration.Kind))
790+
}
791+
}
792+
793+
// promoteImportClause removes the type keyword from an import clause
794+
func promoteImportClause(
795+
changes *change.Tracker,
796+
importClause *ast.ImportClause,
797+
compilerOptions *core.CompilerOptions,
798+
sourceFile *ast.SourceFile,
799+
preferences *lsutil.UserPreferences,
800+
convertExistingToTypeOnly core.Tristate,
801+
aliasDeclaration *ast.Declaration,
802+
) {
803+
// Delete the 'type' keyword
804+
if importClause.PhaseModifier == ast.KindTypeKeyword {
805+
deleteTypeKeyword(changes, sourceFile, importClause.Pos())
806+
}
807+
808+
// Handle .ts extension conversion to .js if necessary
809+
if compilerOptions.AllowImportingTsExtensions.IsFalse() {
810+
moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(importClause.Parent)
811+
if moduleSpecifier != nil {
812+
// Note: We can't check ResolvedUsingTsExtension without program, so we'll skip this optimization
813+
// The fix will still work, just might not change .ts to .js extensions in all cases
814+
}
815+
}
816+
817+
// Handle verbatimModuleSyntax conversion
818+
// If convertExistingToTypeOnly is true, we need to add 'type' to other specifiers
819+
// in the same import declaration
820+
if convertExistingToTypeOnly.IsTrue() {
821+
namedImports := importClause.NamedBindings
822+
if namedImports != nil && namedImports.Kind == ast.KindNamedImports {
823+
namedImportsData := namedImports.AsNamedImports()
824+
if len(namedImportsData.Elements.Nodes) > 1 {
825+
// Check if the list is sorted and if we need to reorder
826+
_, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(
827+
importClause.Parent,
828+
sourceFile,
829+
preferences,
830+
)
831+
832+
// If the alias declaration is an ImportSpecifier and the list is sorted,
833+
// move it to index 0 (since it will be the only non-type-only import)
834+
if isSorted.IsFalse() == false && // isSorted !== false
835+
aliasDeclaration != nil &&
836+
aliasDeclaration.Kind == ast.KindImportSpecifier {
837+
// Find the index of the alias declaration
838+
aliasIndex := -1
839+
for i, element := range namedImportsData.Elements.Nodes {
840+
if element == aliasDeclaration {
841+
aliasIndex = i
842+
break
843+
}
844+
}
845+
// If not already at index 0, move it there
846+
if aliasIndex > 0 {
847+
// Delete the specifier from its current position
848+
changes.Delete(sourceFile, aliasDeclaration)
849+
// Insert it at index 0
850+
changes.InsertImportSpecifierAtIndex(sourceFile, aliasDeclaration, namedImports, 0)
851+
}
852+
}
853+
854+
// Add 'type' keyword to all other import specifiers that aren't already type-only
855+
for _, element := range namedImportsData.Elements.Nodes {
856+
spec := element.AsImportSpecifier()
857+
// Skip the specifier being promoted (if aliasDeclaration is an ImportSpecifier)
858+
if aliasDeclaration != nil && aliasDeclaration.Kind == ast.KindImportSpecifier {
859+
if element == aliasDeclaration {
860+
continue
861+
}
862+
}
863+
// Skip if already type-only
864+
if !spec.IsTypeOnly {
865+
changes.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, element)
866+
}
867+
}
868+
}
869+
}
870+
}
871+
}
872+
873+
// deleteTypeKeyword deletes the 'type' keyword token starting at the given position,
874+
// including any trailing whitespace.
875+
func deleteTypeKeyword(changes *change.Tracker, sourceFile *ast.SourceFile, startPos int) {
876+
scan := scanner.GetScannerForSourceFile(sourceFile, startPos)
877+
if scan.Token() != ast.KindTypeKeyword {
878+
return
879+
}
880+
typeStart := scan.TokenStart()
881+
typeEnd := scan.TokenEnd()
882+
// Skip trailing whitespace
883+
text := sourceFile.Text()
884+
for typeEnd < len(text) && (text[typeEnd] == ' ' || text[typeEnd] == '\t') {
885+
typeEnd++
886+
}
887+
changes.DeleteRange(sourceFile, core.NewTextRange(typeStart, typeEnd))
888+
}
889+
890+
func getModuleSpecifierText(promotedDeclaration *ast.Node) string {
891+
if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration {
892+
importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration()
893+
if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) {
894+
expr := importEqualsDeclaration.ModuleReference.Expression()
895+
if expr != nil && expr.Kind == ast.KindStringLiteral {
896+
return expr.Text()
897+
}
898+
899+
}
900+
return importEqualsDeclaration.ModuleReference.Text()
901+
}
902+
return promotedDeclaration.Parent.ModuleSpecifier().Text()
903+
}
904+
715905
// returns `-1` if `a` is better than `b`
716906
func compareModuleSpecifierRelativity(a *Fix, b *Fix, preferences modulespecifiers.UserPreferences) int {
717907
switch preferences.ImportModuleSpecifierPreference {

internal/ls/codeactions_importfixes.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ var ImportFixProvider = &CodeFixProvider{
5858
}
5959

6060
type fixInfo struct {
61-
// Use new-style fix when available
62-
fix *autoimport.Fix
63-
// Use old-style fix for features not yet ported (UMD, type-only promotion)
64-
oldFix *ImportFix
61+
fix *autoimport.Fix
6562
symbolName string
6663
errorIdentifierText string
6764
isJsxNamespaceFix bool
@@ -391,7 +388,7 @@ func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext, view *autoimport.
391388

392389
// Sort by:
393390
// 1. JSX namespace fixes last
394-
// 2. Fix comparison using view.compareFixes (handles fix kind and module specifier comparison)
391+
// 2. Fix comparison using view.CompareFixes
395392
slices.SortFunc(sorted, func(a, b *fixInfo) int {
396393
// JSX namespace fixes should come last
397394
if cmp := core.CompareBooleans(a.isJsxNamespaceFix, b.isJsxNamespaceFix); cmp != 0 {

0 commit comments

Comments
 (0)