diff --git a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
new file mode 100644
index 00000000000..51267c119fa
--- /dev/null
+++ b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
@@ -0,0 +1,75 @@
+package fourslash_test
+
+import (
+ "testing"
+
+ "github.com/microsoft/typescript-go/internal/fourslash"
+ "github.com/microsoft/typescript-go/internal/testutil"
+)
+
+// Test that auto-imports for JSX tags don't crash when React is type-imported.
+// When both the JSX namespace (React) and the component need to be imported,
+// getSymbolNamesToImport returns multiple names and the type-only promotion
+// path should handle this gracefully instead of panicking.
+func TestCodeFixPromoteTypeOnlyImportJsxTag(t *testing.T) {
+ t.Parallel()
+ defer testutil.RecoverAndFail(t, "Panic on fourslash test")
+ const content = `// @module: preserve
+// @verbatimModuleSyntax: true
+// @jsx: react
+// @Filename: /react.ts
+const React: any = {};
+export default React;
+// @Filename: /bar.tsx
+import type React from "./react";
+
+;`
+ f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
+ defer done()
+ f.GoToMarker(t, "")
+ // The fix should promote the type-only import of React to a regular import.
+ // The "Cannot find name 'Foo'" error does not produce an auto-import for
+ // React since it's already imported (as type-only, handled by promotion).
+ f.VerifyImportFixAtPosition(t, []string{
+ `import React from "./react";
+
+;`,
+ }, nil /*preferences*/)
+}
+
+// Test edge case where both the component name (Foo) and the JSX namespace (React)
+// are type-only imported. Each diagnostic is matched to its symbol via the error
+// message, so each produces only its own promotion fix (no duplicates).
+func TestCodeFixPromoteTypeOnlyImportJsxTagBothTypeOnly(t *testing.T) {
+ t.Parallel()
+ defer testutil.RecoverAndFail(t, "Panic on fourslash test")
+ const content = `// @module: preserve
+// @verbatimModuleSyntax: true
+// @jsx: react
+// @Filename: /react.ts
+const React: any = {};
+export default React;
+// @Filename: /foo.ts
+export function Foo() { return null; }
+// @Filename: /bar.tsx
+import type React from "./react";
+import type { Foo } from "./foo";
+
+;`
+ f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
+ defer done()
+ f.GoToMarker(t, "")
+ // Both Foo and React are type-only imported. The error message string
+ // matching disambiguates which diagnostic is about which symbol, so each
+ // diagnostic produces only its own promotion fix (no duplicates).
+ f.VerifyImportFixAtPosition(t, []string{
+ `import type React from "./react";
+import { Foo } from "./foo";
+
+;`,
+ `import React from "./react";
+import type { Foo } from "./foo";
+
+;`,
+ }, nil /*preferences*/)
+}
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 65ece5f59c5..146fd0970c1 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -3,6 +3,7 @@ package ls
import (
"context"
"slices"
+ "strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
@@ -183,20 +184,33 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
} else if !ast.IsIdentifier(symbolToken) {
return nil, nil
} else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() {
- // Handle type-only import promotion
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
- if len(symbolNames) != 1 {
- panic("Expected exactly one symbol name for type-only import promotion")
+
+ // Best-effort: use the diagnostic message to disambiguate which symbol
+ // the error is about. The message format is "'X' cannot be used as a
+ // value because it was imported using 'import type'." so we check for
+ // the symbol name in single quotes.
+ diagnosticMessage := ""
+ if fixContext.Diagnostic != nil {
+ diagnosticMessage = fixContext.Diagnostic.Message
}
- symbolName := symbolNames[0]
- fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
- if fix != nil {
- return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}}, nil
+
+ for _, sn := range symbolNames {
+ if !sn.isTypeOnly {
+ continue
+ }
+ if diagnosticMessage != "" && !strings.Contains(diagnosticMessage, "'"+sn.name+"'") {
+ continue
+ }
+ fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
+ if fix != nil {
+ info = append(info, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
+ }
}
- return nil, nil
+ return info, nil
} else {
var err error
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
@@ -289,7 +303,13 @@ func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext
// Compute usage position for JSDoc import type fixes
usagePosition := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false)))
- for _, symbolName := range symbolNames {
+ for _, sn := range symbolNames {
+ // Type-only imports are handled by the promotion code path, not the auto-import path.
+ if sn.isTypeOnly {
+ continue
+ }
+
+ symbolName := sn.name
// "default" is a keyword and not a legal identifier for the import
if symbolName == "default" {
continue
@@ -310,8 +330,9 @@ func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext
fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
for _, fix := range fixes {
allInfo = append(allInfo, &fixInfo{
- fix: fix,
- symbolName: symbolName,
+ fix: fix,
+ symbolName: symbolName,
+ isJsxNamespaceFix: symbolName != symbolToken.Text(),
})
}
}
@@ -344,22 +365,40 @@ func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, sy
}
}
-func getSymbolNamesToImport(sourceFile *ast.SourceFile, ch *checker.Checker, symbolToken *ast.Node, compilerOptions *core.CompilerOptions) []string {
+type symbolNameInfo struct {
+ name string
+ isTypeOnly bool // whether the symbol currently resolves to a type-only import
+}
+
+func getSymbolNamesToImport(sourceFile *ast.SourceFile, ch *checker.Checker, symbolToken *ast.Node, compilerOptions *core.CompilerOptions) []symbolNameInfo {
parent := symbolToken.Parent
if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
parent.TagName() == symbolToken &&
jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
jsxNamespace := ch.GetJsxNamespace(sourceFile.AsNode())
if needsJsxNamespaceFix(jsxNamespace, symbolToken, ch) {
- needsComponentNameFix := !scanner.IsIntrinsicJsxName(symbolToken.Text()) &&
- ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, false /* excludeGlobals */) == nil
- if needsComponentNameFix {
- return []string{symbolToken.Text(), jsxNamespace}
+ var result []symbolNameInfo
+ if !scanner.IsIntrinsicJsxName(symbolToken.Text()) {
+ compSymbol := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, false /* excludeGlobals */)
+ if compSymbol == nil {
+ result = append(result, symbolNameInfo{name: symbolToken.Text()})
+ } else if ch.GetTypeOnlyAliasDeclaration(compSymbol) != nil {
+ result = append(result, symbolNameInfo{name: symbolToken.Text(), isTypeOnly: true})
+ }
+ }
+ nsIsTypeOnly := false
+ if nsSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); nsSymbol != nil {
+ nsIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(nsSymbol) != nil
}
- return []string{jsxNamespace}
+ result = append(result, symbolNameInfo{name: jsxNamespace, isTypeOnly: nsIsTypeOnly})
+ return result
}
}
- return []string{symbolToken.Text()}
+ tokenIsTypeOnly := false
+ if sym := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); sym != nil {
+ tokenIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(sym) != nil
+ }
+ return []symbolNameInfo{{name: symbolToken.Text(), isTypeOnly: tokenIsTypeOnly}}
}
func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checker.Checker) bool {
@@ -370,7 +409,6 @@ func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checke
if namespaceSymbol == nil {
return true
}
- // Check if all declarations are type-only
if slices.ContainsFunc(namespaceSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) {
return (namespaceSymbol.Flags & ast.SymbolFlagsValue) == 0
}