From f1fedab46fea3845292cfe1bca3e5442b371fdd3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 21:44:08 +0000
Subject: [PATCH 1/8] Initial plan
From bad009806b89e02179119542b2c6afd2fc3803c8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 21:53:19 +0000
Subject: [PATCH 2/8] fix: handle multiple symbol names in type-only import
promotion for JSX tags
When a JSX tag triggers the "cannot be used as a value because it was
imported using import type" error, getSymbolNamesToImport may return
multiple names (the component name AND the JSX namespace like React).
Instead of panicking when there are multiple names, iterate over all
names attempting type-only promotion for each, and fall back to regular
auto-import logic for names where promotion is not applicable.
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/d390d5fd-644f-4031-8a82-ca0441ed46cb
Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
---
...codeFixPromoteTypeOnlyImportJsxTag_test.go | 46 +++++++++++++++++++
internal/ls/codeactions_importfixes.go | 28 ++++++++---
2 files changed, 68 insertions(+), 6 deletions(-)
create mode 100644 internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
diff --git a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
new file mode 100644
index 00000000000..a6018d0dcaa
--- /dev/null
+++ b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
@@ -0,0 +1,46 @@
+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.
+// https://github.com/microsoft/typescript-go/issues/1234
+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 main goal is that this doesn't panic. The fix should promote
+ // the type-only import of React to a regular import.
+ f.VerifyImportFixAtPosition(t, []string{
+ `import type React from "./react";
+import React from "./react";
+
+;`,
+ `import React from "./react";
+
+;`,
+ `import type React from "./react";
+import React from "./react";
+
+;`,
+ }, nil /*preferences*/)
+}
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 65ece5f59c5..410adb90d2a 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -188,13 +188,29 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
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")
+ for _, symbolName := range symbolNames {
+ fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
+ if fix != nil {
+ info = append(info, &fixInfo{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()})
+ }
}
- 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 any symbol names that couldn't be promoted, fall back to regular auto-import
+ if len(info) < len(symbolNames) {
+ var err error
+ view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
+ if err != nil {
+ return nil, err
+ }
+ if view != nil {
+ info = append(info, getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)...)
+ }
+ }
+ if len(info) > 0 {
+ // Sort fixes by preference
+ if view == nil {
+ view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
+ }
+ return sortFixInfo(info, fixContext, view), nil
}
return nil, nil
} else {
From 81347936353644796504c48cd76eaa7d50d03af0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 17:55:14 +0000
Subject: [PATCH 3/8] simplify type-only promotion to only return promotion
fixes, no auto-import fallback
For the type-only promotion error, iterate over symbol names from
getSymbolNamesToImport and return the first valid promotion fix. Other
missing symbols (like unresolved JSX component names) have their own
separate error diagnostics that trigger auto-import independently.
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/f28dae04-ed90-4058-926b-6ad8d3234cec
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
...codeFixPromoteTypeOnlyImportJsxTag_test.go | 13 +++++-----
internal/ls/codeactions_importfixes.go | 24 ++++---------------
2 files changed, 10 insertions(+), 27 deletions(-)
diff --git a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
index a6018d0dcaa..1b685ad3fda 100644
--- a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
+++ b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
@@ -11,7 +11,6 @@ import (
// 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.
-// https://github.com/microsoft/typescript-go/issues/1234
func TestCodeFixPromoteTypeOnlyImportJsxTag(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
@@ -28,16 +27,16 @@ import type React from "./react";
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.GoToMarker(t, "")
- // The main goal is that this doesn't panic. The fix should promote
- // the type-only import of React to a regular import.
+ // The fix should promote the type-only import of React to a regular import.
+ // Only the promotion fix is returned; the missing "Foo" component is a
+ // separate error handled by a different diagnostic.
f.VerifyImportFixAtPosition(t, []string{
- `import type React from "./react";
-import React from "./react";
-
-;`,
+ // Promotion fix from the "cannot use as value because type-imported" error
`import React from "./react";
;`,
+ // Auto-import fix from the "Cannot find name 'Foo'" error, which also
+ // needs React for JSX
`import type React from "./react";
import React from "./react";
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 410adb90d2a..18bc3b5a408 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -183,7 +183,9 @@ 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
+ // Handle type-only import promotion. getSymbolNamesToImport may return multiple
+ // names for JSX tags (component name + JSX namespace), but only the type-imported
+ // symbol needs promotion here. Other missing symbols get their own error diagnostics.
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
@@ -191,27 +193,9 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
for _, symbolName := range symbolNames {
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
if fix != nil {
- info = append(info, &fixInfo{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()})
+ return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}}, nil
}
}
- // For any symbol names that couldn't be promoted, fall back to regular auto-import
- if len(info) < len(symbolNames) {
- var err error
- view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
- if err != nil {
- return nil, err
- }
- if view != nil {
- info = append(info, getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)...)
- }
- }
- if len(info) > 0 {
- // Sort fixes by preference
- if view == nil {
- view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
- }
- return sortFixInfo(info, fixContext, view), nil
- }
return nil, nil
} else {
var err error
From cfe5f6c3c402f18899eea611cd50624318518c0d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 21:27:22 +0000
Subject: [PATCH 4/8] fix: suppress silly duplicate import, add both-type-only
edge case test
- Type-only promotion path: directly determine JSX candidates (token text +
JSX namespace) instead of using getSymbolNamesToImport, which has
auto-import filtering logic that doesn't apply
- needsJsxNamespaceFix: return false for type-only imports since the
promotion path handles them separately, preventing silly duplicate imports
- getFixesInfoForNonUMDImport: set isJsxNamespaceFix to match Strada
- Add edge case test where both Foo and React are type-only imported
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/a4d3e15d-7971-430e-99c5-81a0ae184f30
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
...codeFixPromoteTypeOnlyImportJsxTag_test.go | 52 ++++++++++++++++---
internal/ls/codeactions_importfixes.go | 32 +++++++-----
2 files changed, 65 insertions(+), 19 deletions(-)
diff --git a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
index 1b685ad3fda..94c2bffc17b 100644
--- a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
+++ b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
@@ -28,17 +28,57 @@ import type React from "./react";
defer done()
f.GoToMarker(t, "")
// The fix should promote the type-only import of React to a regular import.
- // Only the promotion fix is returned; the missing "Foo" component is a
- // separate error handled by a different diagnostic.
+ // 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{
- // Promotion fix from the "cannot use as value because type-imported" error
`import React from "./react";
;`,
- // Auto-import fix from the "Cannot find name 'Foo'" error, which also
- // needs React for JSX
+ }, nil /*preferences*/)
+}
+
+// Test edge case where both the component name (Foo) and the JSX namespace (React)
+// are type-only imported. Both should get promotion fixes. Since there are two
+// diagnostics at the same position (one for each symbol) and we can't distinguish
+// which diagnostic is about which symbol, each diagnostic produces both promotion
+// fixes, resulting in 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. Each of the two diagnostics
+ // produces promotion fixes for both symbols (since we can't match diagnostic
+ // to symbol without parsing the error message), so we get duplicates.
+ f.VerifyImportFixAtPosition(t, []string{
+ `import type React from "./react";
+import { Foo } from "./foo";
+
+;`,
+ `import React from "./react";
+import type { Foo } from "./foo";
+
+;`,
`import type React from "./react";
-import 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 18bc3b5a408..34b36b9f6f4 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -183,20 +183,28 @@ 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. getSymbolNamesToImport may return multiple
- // names for JSX tags (component name + JSX namespace), but only the type-imported
- // symbol needs promotion here. Other missing symbols get their own error diagnostics.
+ // Handle type-only import promotion. For JSX tags, the error could be about
+ // either the component name or the JSX namespace, so check both as candidates.
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
- symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
- for _, symbolName := range symbolNames {
+ candidates := []string{symbolToken.Text()}
+ parent := symbolToken.Parent
+ if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
+ parent.TagName() == symbolToken &&
+ jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
+ jsxNamespace := ch.GetJsxNamespace(fixContext.SourceFile.AsNode())
+ if jsxNamespace != symbolToken.Text() {
+ candidates = append(candidates, jsxNamespace)
+ }
+ }
+ for _, symbolName := range candidates {
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
if fix != nil {
- return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}}, nil
+ info = append(info, &fixInfo{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()})
}
}
- return nil, nil
+ return info, nil
} else {
var err error
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
@@ -310,8 +318,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(),
})
}
}
@@ -370,10 +379,7 @@ 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
- }
+ // Type-only imports are handled by the promotion code path, not the auto-import path.
return false
}
From 32a7b7966d9563aa84fdfca9bd02eadd011ac4c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 22:14:29 +0000
Subject: [PATCH 5/8] refactor: getSymbolNamesToImport returns richer info with
isTypeOnly flag
Modify getSymbolNamesToImport to return symbolNameInfo structs with name
and isTypeOnly fields. This centralizes the JSX checking logic and
allows callers to properly filter:
- Type-only promotion path: filters to isTypeOnly symbols only
- Auto-import path: skips isTypeOnly symbols (handled by promotion)
Also reverts needsJsxNamespaceFix to match Strada behavior (includes
type-only imports in the candidate list) and includes component name
when it resolves as type-only (for the promotion path to use).
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/d5749c27-5468-436b-adb5-cfe32e9f8e35
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
internal/ls/codeactions_importfixes.go | 64 +++++++++++++++++---------
1 file changed, 41 insertions(+), 23 deletions(-)
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 34b36b9f6f4..f208e3ab757 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -183,25 +183,17 @@ 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. For JSX tags, the error could be about
- // either the component name or the JSX namespace, so check both as candidates.
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
- candidates := []string{symbolToken.Text()}
- parent := symbolToken.Parent
- if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
- parent.TagName() == symbolToken &&
- jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
- jsxNamespace := ch.GetJsxNamespace(fixContext.SourceFile.AsNode())
- if jsxNamespace != symbolToken.Text() {
- candidates = append(candidates, jsxNamespace)
+ symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
+ for _, sn := range symbolNames {
+ if !sn.isTypeOnly {
+ continue
}
- }
- for _, symbolName := range candidates {
- fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
+ fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
if fix != nil {
- info = append(info, &fixInfo{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()})
+ info = append(info, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
}
}
return info, nil
@@ -297,7 +289,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
@@ -353,22 +351,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 {
@@ -379,7 +395,9 @@ func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checke
if namespaceSymbol == nil {
return true
}
- // Type-only imports are handled by the promotion code path, not the auto-import path.
+ if slices.ContainsFunc(namespaceSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) {
+ return (namespaceSymbol.Flags & ast.SymbolFlagsValue) == 0
+ }
return false
}
From 4fa2ea3e3d502ccf9b6e3f58d6667818de560836 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 23:22:49 +0000
Subject: [PATCH 6/8] disambiguate type-only promotion fixes via error message
string matching
Use the diagnostic message to correlate which symbol each type-only
promotion 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. This eliminates duplicate fixes
when both component and JSX namespace are type-only imported.
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/eb5910bf-d44c-483e-bdb5-5b6b250eb6e7
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
...codeFixPromoteTypeOnlyImportJsxTag_test.go | 20 +++++--------------
internal/ls/codeactions_importfixes.go | 14 +++++++++++++
2 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
index 94c2bffc17b..51267c119fa 100644
--- a/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
+++ b/internal/fourslash/tests/codeFixPromoteTypeOnlyImportJsxTag_test.go
@@ -38,10 +38,8 @@ import type React from "./react";
}
// Test edge case where both the component name (Foo) and the JSX namespace (React)
-// are type-only imported. Both should get promotion fixes. Since there are two
-// diagnostics at the same position (one for each symbol) and we can't distinguish
-// which diagnostic is about which symbol, each diagnostic produces both promotion
-// fixes, resulting in duplicates.
+// 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")
@@ -61,9 +59,9 @@ 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. Each of the two diagnostics
- // produces promotion fixes for both symbols (since we can't match diagnostic
- // to symbol without parsing the error message), so we get duplicates.
+ // 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";
@@ -72,14 +70,6 @@ import { Foo } from "./foo";
`import React from "./react";
import type { Foo } from "./foo";
-;`,
- `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 f208e3ab757..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"
@@ -187,10 +188,23 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
defer done()
compilerOptions := fixContext.Program.Options()
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
+
+ // 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
+ }
+
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()})
From 1df3b458e79e465c44d9f6ddfa7c3e40ffe23a3f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 May 2026 18:46:51 +0000
Subject: [PATCH 7/8] fix: fall back to all type-only fixes when message filter
matches none
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/1c2ae9fb-44fd-48a5-b940-a39404eb47f0
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
internal/ls/codeactions_importfixes.go | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 146fd0970c1..9c1e361dd0e 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -198,18 +198,31 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
diagnosticMessage = fixContext.Diagnostic.Message
}
+ // Collect all type-only candidates with valid fixes.
+ var allTypeOnlyFixes []*fixInfo
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()})
+ allTypeOnlyFixes = append(allTypeOnlyFixes, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
}
}
+
+ // If there are multiple type-only candidates, try to disambiguate
+ // using the diagnostic message (which quotes the symbol name).
+ // Fall back to all candidates if filtering yields nothing.
+ if len(allTypeOnlyFixes) > 1 && diagnosticMessage != "" {
+ for _, fi := range allTypeOnlyFixes {
+ if strings.Contains(diagnosticMessage, "'"+fi.symbolName+"'") {
+ info = append(info, fi)
+ }
+ }
+ }
+ if len(info) == 0 {
+ info = allTypeOnlyFixes
+ }
return info, nil
} else {
var err error
From 6e3541d027a95a930a54fbf3c6c634abd13444ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 17:14:15 +0000
Subject: [PATCH 8/8] refactor: consolidate disambiguation comment at filtering
site
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/02331959-bd9f-478f-85c5-7dafe97b4304
Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
---
internal/ls/codeactions_importfixes.go | 22 +++++++++-------------
1 file changed, 9 insertions(+), 13 deletions(-)
diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go
index 9c1e361dd0e..b006cf7f3a3 100644
--- a/internal/ls/codeactions_importfixes.go
+++ b/internal/ls/codeactions_importfixes.go
@@ -189,16 +189,6 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
compilerOptions := fixContext.Program.Options()
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
- // 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
- }
-
- // Collect all type-only candidates with valid fixes.
var allTypeOnlyFixes []*fixInfo
for _, sn := range symbolNames {
if !sn.isTypeOnly {
@@ -210,9 +200,15 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
}
}
- // If there are multiple type-only candidates, try to disambiguate
- // using the diagnostic message (which quotes the symbol name).
- // Fall back to all candidates if filtering yields nothing.
+ // For JSX opening tags, there can be separate type-only errors for both the tag name
+ // identifier and the JSX namespace identifier. When both produce valid fixes, we
+ // disambiguate using the diagnostic message, which quotes the symbol name in single
+ // quotes (e.g., "'React' cannot be used as a value..."). If filtering yields nothing
+ // (e.g., due to localization), fall back to returning all candidates.
+ diagnosticMessage := ""
+ if fixContext.Diagnostic != nil {
+ diagnosticMessage = fixContext.Diagnostic.Message
+ }
if len(allTypeOnlyFixes) > 1 && diagnosticMessage != "" {
for _, fi := range allTypeOnlyFixes {
if strings.Contains(diagnosticMessage, "'"+fi.symbolName+"'") {