Skip to content

Commit 6a19dd0

Browse files
authored
Prevent deadlock in getCompletionsAtPosition(..., { includeSymbol: true }) API (#64178)
1 parent 8bf449b commit 6a19dd0

9 files changed

Lines changed: 120 additions & 56 deletions

tsc/internal/api/session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,7 @@ func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImpo
22852285
sourceFile,
22862286
projectPath,
22872287
program,
2288+
ch,
22882289
userPreferences.ModuleSpecifierPreferences(),
22892290
)
22902291
importAdder := autoimport.NewImportAdder(

tsc/internal/api/session_completion_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/microsoft/TypeScript/tsc/internal/bundled"
78
"github.com/microsoft/TypeScript/tsc/internal/core"
@@ -185,3 +186,71 @@ func TestCompletionRetriesWithAutoImports(t *testing.T) {
185186
}
186187
t.Fatal("expected auto-import completion for someValue")
187188
}
189+
190+
func TestCompletionWithSymbolsAndExistingImportDoesNotDeadlock(t *testing.T) {
191+
t.Parallel()
192+
if !bundled.Embedded {
193+
t.Skip("bundled files are not embedded")
194+
}
195+
196+
const fileName = "/home/projects/p/src/index.ts"
197+
const content = "import { otherValue } from \"./export\";\nsomeV"
198+
projectSession, _ := projecttestutil.Setup(map[string]any{
199+
"/home/projects/p/tsconfig.json": `{ "compilerOptions": { "module": "esnext", "target": "esnext" } }`,
200+
"/home/projects/p/src/export.ts": "export const otherValue = 0; export const someValue = 1;",
201+
fileName: content,
202+
})
203+
defer projectSession.Close()
204+
projectSession.Configure(lsutil.UserPreferences{
205+
IncludeCompletionsForModuleExports: core.TSTrue,
206+
IncludeCompletionsForImportStatements: core.TSTrue,
207+
})
208+
209+
session := NewLSPSession(projectSession, nil)
210+
defer session.Close()
211+
212+
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
213+
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
214+
})
215+
assert.NilError(t, err)
216+
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
217+
Snapshot: snapshotResp.Snapshot,
218+
File: DocumentIdentifier{FileName: fileName},
219+
})
220+
assert.NilError(t, err)
221+
assert.Assert(t, proj != nil, "file should resolve to a default project")
222+
223+
// IncludeSymbol pins completion to the single persistent API checker. When
224+
// ranking the auto-import completion, the existing import makes the view
225+
// consult that checker. This used to try to acquire the same checker again
226+
// and deadlock.
227+
type completionResult struct {
228+
completions *CompletionInfoResponse
229+
err error
230+
}
231+
result := make(chan completionResult, 1)
232+
go func() {
233+
completions, e := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
234+
Snapshot: snapshotResp.Snapshot,
235+
Project: proj.Id,
236+
File: DocumentIdentifier{FileName: fileName},
237+
Position: uint32(len(content)),
238+
IncludeSymbol: true,
239+
})
240+
result <- completionResult{completions: completions, err: e}
241+
}()
242+
243+
select {
244+
case completion := <-result:
245+
assert.NilError(t, completion.err)
246+
assert.Assert(t, completion.completions != nil, "expected a completion list")
247+
for _, entry := range completion.completions.Entries {
248+
if entry.Name == "someValue" {
249+
return
250+
}
251+
}
252+
t.Fatal("expected auto-import completion for someValue")
253+
case <-time.After(10 * time.Second):
254+
t.Fatal("completion request deadlocked while examining an existing import")
255+
}
256+
}

tsc/internal/ls/autoimport/fix.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -551,13 +551,13 @@ func makeImport(ct *change.Tracker, defaultImport *ast.IdentifierNode, namedImpo
551551
return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/)
552552
}
553553

554-
func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isValidTypeOnlyUseSite bool, usagePosition *lsproto.Position) []*Fix {
554+
func (v *View) GetFixes(export *Export, forJSX bool, isValidTypeOnlyUseSite bool, usagePosition *lsproto.Position) []*Fix {
555555
var fixes []*Fix
556-
if namespaceFix := v.tryUseExistingNamespaceImport(ctx, export, usagePosition); namespaceFix != nil {
556+
if namespaceFix := v.tryUseExistingNamespaceImport(export, usagePosition); namespaceFix != nil {
557557
fixes = append(fixes, namespaceFix)
558558
}
559559

560-
if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite); fix != nil {
560+
if fix := v.tryAddToExistingImport(export, isValidTypeOnlyUseSite); fix != nil {
561561
return append(fixes, fix)
562562
}
563563

@@ -633,7 +633,7 @@ func getAddAsTypeOnly(isValidTypeOnlyUseSite bool, export *Export, compilerOptio
633633
return lsproto.AddAsTypeOnlyAllowed
634634
}
635635

636-
func (v *View) tryUseExistingNamespaceImport(ctx context.Context, export *Export, usagePosition *lsproto.Position) *Fix {
636+
func (v *View) tryUseExistingNamespaceImport(export *Export, usagePosition *lsproto.Position) *Fix {
637637
if usagePosition == nil {
638638
return nil
639639
}
@@ -642,7 +642,7 @@ func (v *View) tryUseExistingNamespaceImport(ctx context.Context, export *Export
642642
return nil
643643
}
644644

645-
existingImports := v.getExistingImports(ctx)
645+
existingImports := v.getExistingImports()
646646
matchingDeclarations := existingImports.Get(export.ModuleID)
647647
for _, existingImport := range matchingDeclarations {
648648
namespacePrefix := getNamespaceLikeImportText(existingImport.node)
@@ -688,11 +688,10 @@ func getNamespaceLikeImportText(declaration *ast.Node) string {
688688
}
689689

690690
func (v *View) tryAddToExistingImport(
691-
ctx context.Context,
692691
export *Export,
693692
isValidTypeOnlyUseSite bool,
694693
) *Fix {
695-
existingImports := v.getExistingImports(ctx)
694+
existingImports := v.getExistingImports()
696695
matchingDeclarations := existingImports.Get(export.ModuleID)
697696
if len(matchingDeclarations) == 0 {
698697
return nil
@@ -844,27 +843,25 @@ type existingImport struct {
844843
index int
845844
}
846845

847-
func (v *View) getExistingImports(ctx context.Context) *collections.MultiMap[ModuleID, existingImport] {
846+
func (v *View) getExistingImports() *collections.MultiMap[ModuleID, existingImport] {
848847
if v.existingImports != nil {
849848
return v.existingImports
850849
}
851850

852851
result := collections.NewMultiMapWithSizeHint[ModuleID, existingImport](len(v.importingFile.Imports()))
853-
ch, done := v.program.GetTypeChecker(ctx)
854-
defer done()
855852

856853
for i, moduleSpecifier := range v.importingFile.Imports() {
857854
node := ast.TryGetImportFromModuleSpecifier(moduleSpecifier)
858855
if node == nil {
859856
panic("error: did not expect node kind " + moduleSpecifier.Kind.String())
860857
} else if ast.IsVariableDeclarationInitializedToRequire(node.Parent) {
861-
if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier, nil /*importAttributesType*/); moduleSymbol != nil {
858+
if moduleSymbol := v.checker.ResolveExternalModuleName(moduleSpecifier, nil /*importAttributesType*/); moduleSymbol != nil {
862859
if moduleID, _, ok := tryGetModuleIDAndFileNameOfModuleSymbol(moduleSymbol); ok {
863860
result.Add(moduleID, existingImport{node: node.Parent, moduleSpecifier: moduleSpecifier.Text(), index: i})
864861
}
865862
}
866863
} else if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindImportEqualsDeclaration || node.Kind == ast.KindJSDocImportTag {
867-
if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil {
864+
if moduleSymbol := v.checker.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil {
868865
if moduleID, _, ok := tryGetModuleIDAndFileNameOfModuleSymbol(moduleSymbol); ok {
869866
result.Add(moduleID, existingImport{node: node, moduleSpecifier: moduleSpecifier.Text(), index: i})
870867
}

tsc/internal/ls/autoimport/import_adder.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol
108108
// debug.Assert(len(adder.ls.UserPreferences().AutoImportFileExcludePatterns) > 0)
109109
return
110110
}
111-
fix := adder.getImportFixForSymbol(adder.view, adder.view.importingFile, exportInfos, isValidTypeOnlyUseSite)
111+
fix := adder.getImportFixForSymbol(adder.view, exportInfos, isValidTypeOnlyUseSite)
112112
if fix != nil {
113113
// !!! referenceImport -> propertyName
114114
adder.AddImportFix(fix)
@@ -488,9 +488,9 @@ func replaceFirstIdentifierOfEntityName(factory *ast.NodeFactory, name *ast.Enti
488488
)
489489
}
490490

491-
func (adder *importAdder) getImportFixForSymbol(view *View, file *ast.SourceFile, exports []*Export, isValidTypeOnlyUseSite bool) *Fix {
491+
func (adder *importAdder) getImportFixForSymbol(view *View, exports []*Export, isValidTypeOnlyUseSite bool) *Fix {
492492
fixes := core.FlatMap(exports, func(export *Export) []*Fix {
493-
return view.GetFixes(adder.ctx, export, false /*forJSX*/, isValidTypeOnlyUseSite, nil /*usagePosition*/)
493+
return view.GetFixes(export, false /*forJSX*/, isValidTypeOnlyUseSite, nil /*usagePosition*/)
494494
})
495495
slices.SortFunc(fixes, func(a, b *Fix) int {
496496
return view.CompareFixesForRanking(a, b)

tsc/internal/ls/autoimport/view.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package autoimport
22

33
import (
4-
"context"
54
"slices"
65
"strings"
76
"unicode"
87

98
"github.com/microsoft/TypeScript/tsc/internal/ast"
9+
"github.com/microsoft/TypeScript/tsc/internal/checker"
1010
"github.com/microsoft/TypeScript/tsc/internal/collections"
1111
"github.com/microsoft/TypeScript/tsc/internal/compiler"
1212
"github.com/microsoft/TypeScript/tsc/internal/core"
@@ -23,6 +23,7 @@ type View struct {
2323
importingFile *ast.SourceFile
2424
importingFilePath tspath.Path
2525
program *compiler.Program
26+
checker *checker.Checker
2627
preferences modulespecifiers.UserPreferences
2728
projectKey tspath.Path
2829

@@ -33,7 +34,7 @@ type View struct {
3334
shouldUseRequireForFixes *bool
3435
}
3536

36-
func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View {
37+
func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, typeChecker *checker.Checker, preferences modulespecifiers.UserPreferences) *View {
3738
importingFilePath := importingFile.Path()
3839
if canonical := importingFile.CanonicalSourceFile(); canonical != nil {
3940
importingFilePath = canonical.Path()
@@ -43,6 +44,7 @@ func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspat
4344
importingFile: importingFile,
4445
importingFilePath: importingFilePath,
4546
program: program,
47+
checker: typeChecker,
4648
projectKey: projectKey,
4749
preferences: preferences,
4850
conditions: collections.NewSetFromItems(
@@ -175,7 +177,7 @@ type FixAndExport struct {
175177
Export *Export
176178
}
177179

178-
func (v *View) GetCompletions(ctx context.Context, prefix string, position lsproto.Position, forJSX bool, isTypeOnlyLocation bool) []*FixAndExport {
180+
func (v *View) GetCompletions(prefix string, position lsproto.Position, forJSX bool, isTypeOnlyLocation bool) []*FixAndExport {
179181
results := v.Search(prefix, QueryKindWordPrefix)
180182

181183
type exportGroupKey struct {
@@ -240,7 +242,7 @@ outer:
240242
for _, exps := range grouped {
241243
fixesForGroup := make([]*FixAndExport, 0, len(exps))
242244
for _, e := range exps {
243-
for _, fix := range v.GetFixes(ctx, e, forJSX, isTypeOnlyLocation, &position) {
245+
for _, fix := range v.GetFixes(e, forJSX, isTypeOnlyLocation, &position) {
244246
fixesForGroup = append(fixesForGroup, &FixAndExport{
245247
Fix: fix,
246248
Export: e,

tsc/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func getInheritedMembers(typeChecker *checker.Checker, classDeclaration *ast.Nod
229229
}
230230

231231
func createImportAdder(context context.Context, fixContext *CodeFixContext, typeChecker *checker.Checker) (autoimport.ImportAdder, error) {
232-
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
232+
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile, typeChecker)
233233
if err != nil {
234234
return nil, err
235235
}

tsc/internal/ls/codeactions_importfixes.go

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/microsoft/TypeScript/tsc/internal/ast"
99
"github.com/microsoft/TypeScript/tsc/internal/astnav"
1010
"github.com/microsoft/TypeScript/tsc/internal/checker"
11-
"github.com/microsoft/TypeScript/tsc/internal/compiler"
1211
"github.com/microsoft/TypeScript/tsc/internal/core"
1312
"github.com/microsoft/TypeScript/tsc/internal/diagnostics"
1413
"github.com/microsoft/TypeScript/tsc/internal/locale"
@@ -111,17 +110,17 @@ func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*
111110
return nil, nil
112111
}
113112

114-
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
113+
ch, done := fixContext.Program.GetTypeChecker(ctx)
114+
defer done()
115+
116+
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile, ch)
115117
if err != nil {
116118
return nil, err
117119
}
118120
if view == nil {
119-
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
121+
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile, ch)
120122
}
121123

122-
ch, done := fixContext.Program.GetTypeChecker(ctx)
123-
defer done()
124-
125124
importAdder := autoimport.NewImportAdder(
126125
ctx,
127126
fixContext.Program,
@@ -176,18 +175,20 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
176175
}
177176

178177
symbolToken := astnav.GetTokenAtPosition(fixContext.SourceFile, pos)
178+
if errorCode != diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() && !ast.IsIdentifier(symbolToken) {
179+
return nil, nil
180+
}
181+
182+
ch, done := fixContext.Program.GetTypeChecker(ctx)
183+
defer done()
179184

180185
var view *autoimport.View
181186
var info []*fixInfo
182187

183188
if errorCode == diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() {
184-
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
185-
info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken, view)
186-
} else if !ast.IsIdentifier(symbolToken) {
187-
return nil, nil
189+
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile, ch)
190+
info = getFixesInfoForUMDImport(symbolToken, view, ch)
188191
} else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() {
189-
ch, done := fixContext.Program.GetTypeChecker(ctx)
190-
defer done()
191192
compilerOptions := fixContext.Program.Options()
192193
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
193194

@@ -196,7 +197,7 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
196197
if !sn.isTypeOnly {
197198
continue
198199
}
199-
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
200+
fix := getTypeOnlyPromotionFix(fixContext.SourceFile, symbolToken, sn.name, ch)
200201
if fix != nil {
201202
allTypeOnlyFixes = append(allTypeOnlyFixes, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
202203
}
@@ -224,26 +225,23 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3
224225
return info, nil
225226
} else {
226227
var err error
227-
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
228+
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile, ch)
228229
if err != nil {
229230
return nil, err
230231
}
231232
if view != nil {
232-
info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)
233+
info = getFixesInfoForNonUMDImport(fixContext, symbolToken, view, ch)
233234
}
234235
}
235236

236237
// Sort fixes by preference
237238
if view == nil {
238-
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
239+
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile, ch)
239240
}
240241
return sortFixInfo(info, fixContext, view), nil
241242
}
242243

243-
func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node, view *autoimport.View) []*fixInfo {
244-
ch, done := fixContext.Program.GetTypeChecker(ctx)
245-
defer done()
246-
244+
func getFixesInfoForUMDImport(token *ast.Node, view *autoimport.View, ch *checker.Checker) []*fixInfo {
247245
umdSymbol := getUmdSymbol(token, ch)
248246
if umdSymbol == nil {
249247
return nil
@@ -253,7 +251,7 @@ func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, t
253251
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(token)
254252

255253
var result []*fixInfo
256-
for _, fix := range view.GetFixes(ctx, export, false, isValidTypeOnlyUseSite, nil) {
254+
for _, fix := range view.GetFixes(export, false, isValidTypeOnlyUseSite, nil) {
257255
errorIdentifierText := ""
258256
if ast.IsIdentifier(token) {
259257
errorIdentifierText = token.Text()
@@ -302,9 +300,7 @@ func isUMDExportSymbol(symbol *ast.Symbol) bool {
302300
ast.IsNamespaceExportDeclaration(symbol.Declarations[0])
303301
}
304302

305-
func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View) []*fixInfo {
306-
ch, done := fixContext.Program.GetTypeChecker(ctx)
307-
defer done()
303+
func getFixesInfoForNonUMDImport(fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View, ch *checker.Checker) []*fixInfo {
308304
compilerOptions := fixContext.Program.Options()
309305

310306
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken)
@@ -341,7 +337,7 @@ func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext
341337
continue
342338
}
343339

344-
fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
340+
fixes := view.GetFixes(export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
345341
for _, fix := range fixes {
346342
allInfo = append(allInfo, &fixInfo{
347343
fix: fix,
@@ -355,10 +351,7 @@ func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext
355351
return allInfo
356352
}
357353

358-
func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *autoimport.Fix {
359-
ch, done := program.GetTypeChecker(ctx)
360-
defer done()
361-
354+
func getTypeOnlyPromotionFix(sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, ch *checker.Checker) *autoimport.Fix {
362355
// Get the symbol at the token location
363356
symbol := ch.ResolveName(symbolName, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
364357
if symbol == nil {

0 commit comments

Comments
 (0)