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