Skip to content

Commit 675a3f4

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 98b622b commit 675a3f4

File tree

2 files changed

+272
-68
lines changed

2 files changed

+272
-68
lines changed

internal/ls/autoimport/fix.go

Lines changed: 193 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
)
@@ -38,6 +40,9 @@ type Fix struct {
3840
ModuleSpecifierKind modulespecifiers.ResultKind
3941
IsReExport bool
4042
ModuleFileName string
43+
44+
// For PromoteTypeOnly fixes
45+
TypeOnlyAliasDeclaration *ast.Declaration
4146
}
4247

4348
func (f *Fix) Edits(
@@ -105,6 +110,14 @@ func (f *Fix) Edits(
105110
// addNamespaceQualifier(tracker, file, qualification)
106111
// }
107112
return tracker.GetChanges()[file.FileName()], diagnostics.Add_import_from_0.Format(f.ModuleSpecifier)
113+
case lsproto.AutoImportFixKindPromoteTypeOnly:
114+
promotedDeclaration := promoteFromTypeOnly(tracker, f.TypeOnlyAliasDeclaration, compilerOptions, file, preferences)
115+
if promotedDeclaration.Kind == ast.KindImportSpecifier {
116+
moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent)
117+
return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_of_0_from_1.Format(f.Name, moduleSpec)
118+
}
119+
moduleSpec := getModuleSpecifierText(promotedDeclaration)
120+
return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_declaration_from_0.Format(moduleSpec)
108121
default:
109122
panic("unimplemented fix edit")
110123
}
@@ -711,6 +724,186 @@ func isIndexFileName(fileName string) bool {
711724
return fileName == "index"
712725
}
713726

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

0 commit comments

Comments
 (0)