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`
716906func compareModuleSpecifierRelativity (a * Fix , b * Fix , preferences modulespecifiers.UserPreferences ) int {
717907 switch preferences .ImportModuleSpecifierPreference {
0 commit comments