Skip to content

Commit c99380f

Browse files
authored
Fix auto-import completions sometimes not updating existing imports (microsoft#48815)
1 parent 07660c8 commit c99380f

File tree

3 files changed

+92
-27
lines changed

3 files changed

+92
-27
lines changed

src/services/codefixes/importFixes.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ namespace ts.codefix {
8383
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
8484
const exportInfo = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, /*isJsxTagName*/ false, host, program, preferences, useAutoImportProvider);
8585
const useRequire = shouldUseRequire(sourceFile, program);
86-
const fix = getImportFixForSymbol(sourceFile, exportInfo, moduleSymbol, symbolName, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
86+
const fix = getImportFixForSymbol(sourceFile, exportInfo, moduleSymbol, program, /*useNamespaceInfo*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
8787
if (fix) {
8888
addImport({ fixes: [fix], symbolName, errorIdentifierText: undefined });
8989
}
@@ -310,7 +310,7 @@ namespace ts.codefix {
310310
: getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, isJsxTagName, host, program, preferences, /*useAutoImportProvider*/ true);
311311
const useRequire = shouldUseRequire(sourceFile, program);
312312
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
313-
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, isValidTypeOnlyUseSite, useRequire, host, preferences));
313+
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, program, { symbolName, position }, isValidTypeOnlyUseSite, useRequire, host, preferences));
314314
return {
315315
moduleSpecifier: fix.moduleSpecifier,
316316
codeAction: codeFixActionToCodeAction(codeActionForFix(
@@ -331,10 +331,10 @@ namespace ts.codefix {
331331
return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, QuotePreference.Double, compilerOptions));
332332
}
333333

334-
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
334+
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, program: Program, useNamespaceInfo: { position: number, symbolName: string } | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
335335
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol || info.symbol.parent === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
336336
const packageJsonImportFilter = createPackageJsonImportFilter(sourceFile, preferences, host);
337-
return getBestFix(getImportFixes(exportInfos, symbolName, position, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences), sourceFile, program, packageJsonImportFilter, host);
337+
return getBestFix(getImportFixes(exportInfos, useNamespaceInfo, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences).fixes, sourceFile, program, packageJsonImportFilter, host);
338338
}
339339

340340
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
@@ -396,20 +396,23 @@ namespace ts.codefix {
396396

397397
export function getModuleSpecifierForBestExportInfo(
398398
exportInfo: readonly SymbolExportInfo[],
399+
symbolName: string,
400+
position: number,
401+
isValidTypeOnlyUseSite: boolean,
399402
importingFile: SourceFile,
400403
program: Program,
401404
host: LanguageServiceHost,
402405
preferences: UserPreferences,
403406
packageJsonImportFilter?: PackageJsonImportFilter,
404407
fromCacheOnly?: boolean,
405408
): { exportInfo?: SymbolExportInfo, moduleSpecifier: string, computedWithoutCacheCount: number } | undefined {
406-
const { fixes, computedWithoutCacheCount } = getNewImportFixes(
409+
const { fixes, computedWithoutCacheCount } = getImportFixes(
410+
exportInfo,
411+
{ symbolName, position },
412+
isValidTypeOnlyUseSite,
413+
/*useRequire*/ false,
407414
program,
408415
importingFile,
409-
/*position*/ undefined,
410-
/*isValidTypeOnlyUseSite*/ false,
411-
/*useRequire*/ false,
412-
exportInfo,
413416
host,
414417
preferences,
415418
fromCacheOnly);
@@ -419,23 +422,46 @@ namespace ts.codefix {
419422

420423
function getImportFixes(
421424
exportInfos: readonly SymbolExportInfo[],
422-
symbolName: string,
425+
useNamespaceInfo: {
426+
symbolName: string,
427+
position: number,
428+
} | undefined,
423429
/** undefined only for missing JSX namespace */
424-
position: number | undefined,
425430
isValidTypeOnlyUseSite: boolean,
426431
useRequire: boolean,
427432
program: Program,
428433
sourceFile: SourceFile,
429434
host: LanguageServiceHost,
430435
preferences: UserPreferences,
431-
): readonly ImportFixWithModuleSpecifier[] {
436+
fromCacheOnly?: boolean,
437+
): { computedWithoutCacheCount: number, fixes: readonly ImportFixWithModuleSpecifier[] } {
432438
const checker = program.getTypeChecker();
433439
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile, program.getCompilerOptions()));
434-
const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker);
440+
const useNamespace = useNamespaceInfo && tryUseExistingNamespaceImport(existingImports, useNamespaceInfo.symbolName, useNamespaceInfo.position, checker);
435441
const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions());
436-
// Don't bother providing an action to add a new import if we can add to an existing one.
437-
const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, isValidTypeOnlyUseSite, useRequire, host, preferences);
438-
return [...(useNamespace ? [useNamespace] : emptyArray), ...addImport];
442+
if (addToExisting) {
443+
// Don't bother providing an action to add a new import if we can add to an existing one.
444+
return {
445+
computedWithoutCacheCount: 0,
446+
fixes: [...(useNamespace ? [useNamespace] : emptyArray), addToExisting],
447+
};
448+
}
449+
450+
const { fixes, computedWithoutCacheCount = 0 } = getFixesForAddImport(
451+
exportInfos,
452+
existingImports,
453+
program,
454+
sourceFile,
455+
useNamespaceInfo?.position,
456+
isValidTypeOnlyUseSite,
457+
useRequire,
458+
host,
459+
preferences,
460+
fromCacheOnly);
461+
return {
462+
computedWithoutCacheCount,
463+
fixes: [...(useNamespace ? [useNamespace] : emptyArray), ...fixes],
464+
};
439465
}
440466

441467
function tryUseExistingNamespaceImport(existingImports: readonly FixAddToExistingImportInfo[], symbolName: string, position: number, checker: TypeChecker): FixUseNamespaceImport | undefined {
@@ -658,9 +684,10 @@ namespace ts.codefix {
658684
useRequire: boolean,
659685
host: LanguageServiceHost,
660686
preferences: UserPreferences,
661-
): readonly (FixAddNewImport | FixAddJsdocTypeImport)[] {
687+
fromCacheOnly?: boolean,
688+
): { computedWithoutCacheCount?: number, fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[] } {
662689
const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, isValidTypeOnlyUseSite, useRequire, program.getTypeChecker(), program.getCompilerOptions()));
663-
return existingDeclaration ? [existingDeclaration] : getNewImportFixes(program, sourceFile, position, isValidTypeOnlyUseSite, useRequire, exportInfos, host, preferences).fixes;
690+
return existingDeclaration ? { fixes: [existingDeclaration] } : getNewImportFixes(program, sourceFile, position, isValidTypeOnlyUseSite, useRequire, exportInfos, host, preferences, fromCacheOnly);
664691
}
665692

666693
function newImportInfoFromExistingSpecifier(
@@ -782,7 +809,8 @@ namespace ts.codefix {
782809
const symbolName = umdSymbol.name;
783810
const exportInfo: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, moduleFileName: undefined, exportKind: ExportKind.UMD, targetFlags: symbol.flags, isFromPackageJson: false }];
784811
const useRequire = shouldUseRequire(sourceFile, program);
785-
const fixes = getImportFixes(exportInfo, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*isValidTypeOnlyUseSite*/ false, useRequire, program, sourceFile, host, preferences);
812+
const position = isIdentifier(token) ? token.getStart(sourceFile) : undefined;
813+
const fixes = getImportFixes(exportInfo, position ? { position, symbolName } : undefined, /*isValidTypeOnlyUseSite*/ false, useRequire, program, sourceFile, host, preferences).fixes;
786814
return { fixes, symbolName, errorIdentifierText: tryCast(token, isIdentifier)?.text };
787815
}
788816
function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined {
@@ -855,7 +883,7 @@ namespace ts.codefix {
855883
const useRequire = shouldUseRequire(sourceFile, program);
856884
const exportInfo = getExportInfos(symbolName, isJSXTagName(symbolToken), getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
857885
const fixes = arrayFrom(flatMapIterator(exportInfo.entries(), ([_, exportInfos]) =>
858-
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences)));
886+
getImportFixes(exportInfos, { symbolName, position: symbolToken.getStart(sourceFile) }, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences).fixes));
859887
return { fixes, symbolName, errorIdentifierText: symbolToken.text };
860888
}
861889

src/services/completions.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ namespace ts.Completions {
170170
const enum GlobalsSearch { Continue, Success, Fail }
171171

172172
interface ModuleSpecifierResolutioContext {
173-
tryResolve: (exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined;
173+
tryResolve: (exportInfo: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined;
174174
resolutionLimitExceeded: () => boolean;
175175
}
176176

@@ -184,8 +184,10 @@ namespace ts.Completions {
184184
host: LanguageServiceHost,
185185
program: Program,
186186
sourceFile: SourceFile,
187+
position: number,
187188
preferences: UserPreferences,
188189
isForImportStatementCompletion: boolean,
190+
isValidTypeOnlyUseSite: boolean,
189191
cb: (context: ModuleSpecifierResolutioContext) => TReturn,
190192
): TReturn {
191193
const start = timestamp();
@@ -204,9 +206,9 @@ namespace ts.Completions {
204206
host.log?.(`${logPrefix}: ${timestamp() - start}`);
205207
return result;
206208

207-
function tryResolve(exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined {
209+
function tryResolve(exportInfo: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined {
208210
if (isFromAmbientModule) {
209-
const result = codefix.getModuleSpecifierForBestExportInfo(exportInfo, sourceFile, program, host, preferences);
211+
const result = codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences);
210212
if (result) {
211213
ambientCount++;
212214
}
@@ -215,7 +217,7 @@ namespace ts.Completions {
215217
const shouldResolveModuleSpecifier = isForImportStatementCompletion || preferences.allowIncompleteCompletions && resolvedCount < moduleSpecifierResolutionLimit;
216218
const shouldGetModuleSpecifierFromCache = !shouldResolveModuleSpecifier && preferences.allowIncompleteCompletions && cacheAttemptCount < moduleSpecifierResolutionCacheAttemptLimit;
217219
const result = (shouldResolveModuleSpecifier || shouldGetModuleSpecifierFromCache)
218-
? codefix.getModuleSpecifierForBestExportInfo(exportInfo, sourceFile, program, host, preferences, packageJsonImportFilter, shouldGetModuleSpecifierFromCache)
220+
? codefix.getModuleSpecifierForBestExportInfo(exportInfo, symbolName, position, isValidTypeOnlyUseSite, sourceFile, program, host, preferences, packageJsonImportFilter, shouldGetModuleSpecifierFromCache)
219221
: undefined;
220222

221223
if (!shouldResolveModuleSpecifier && !shouldGetModuleSpecifierFromCache || shouldGetModuleSpecifierFromCache && !result) {
@@ -358,8 +360,10 @@ namespace ts.Completions {
358360
host,
359361
program,
360362
file,
363+
location.getStart(),
361364
preferences,
362365
/*isForImportStatementCompletion*/ false,
366+
isValidTypeOnlyAliasUseSite(location),
363367
context => {
364368
const entries = mapDefined(previousResponse.entries, entry => {
365369
if (!entry.hasAction || !entry.source || !entry.data || completionEntryDataIsResolved(entry.data)) {
@@ -374,7 +378,7 @@ namespace ts.Completions {
374378
const { origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
375379
const info = exportMap.get(file.path, entry.data.exportMapKey);
376380

377-
const result = info && context.tryResolve(info, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name)));
381+
const result = info && context.tryResolve(info, entry.name, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name)));
378382
if (!result) return entry;
379383

380384
const newOrigin: SymbolOriginInfoResolvedExport = {
@@ -2428,7 +2432,7 @@ namespace ts.Completions {
24282432
moduleSymbol,
24292433
symbol: firstAccessibleSymbol,
24302434
targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags,
2431-
}], sourceFile, program, host, preferences) || {};
2435+
}], firstAccessibleSymbol.name, position, isValidTypeOnlyAliasUseSite(location), sourceFile, program, host, preferences) || {};
24322436

24332437
if (moduleSpecifier) {
24342438
const origin: SymbolOriginInfoResolvedExport = {
@@ -2707,8 +2711,10 @@ namespace ts.Completions {
27072711
host,
27082712
program,
27092713
sourceFile,
2714+
position,
27102715
preferences,
27112716
!!importCompletionNode,
2717+
isValidTypeOnlyAliasUseSite(location),
27122718
context => {
27132719
exportInfo.search(
27142720
sourceFile.path,
@@ -2737,7 +2743,7 @@ namespace ts.Completions {
27372743

27382744
// If we don't need to resolve module specifiers, we can use any re-export that is importable at all
27392745
// (We need to ensure that at least one is importable to show a completion.)
2740-
const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {};
2746+
const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, symbolName, isFromAmbientModule) || {};
27412747
const isDefaultExport = exportInfo.exportKind === ExportKind.Default;
27422748
const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol;
27432749

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: commonjs
4+
5+
// @Filename: /deep/module/why/you/want/this/path.ts
6+
//// export const x = 0;
7+
//// export const y = 1;
8+
9+
// @Filename: /nice/reexport.ts
10+
//// import { x, y } from "../deep/module/why/you/want/this/path";
11+
//// export { x, y };
12+
13+
// @Filename: /index.ts
14+
//// import { x } from "./deep/module/why/you/want/this/path";
15+
////
16+
//// y/**/
17+
18+
verify.completions({
19+
marker: "",
20+
exact: completion.globalsPlus(["x", {
21+
name: "y",
22+
source: "./deep/module/why/you/want/this/path",
23+
sourceDisplay: "./deep/module/why/you/want/this/path",
24+
sortText: completion.SortText.AutoImportSuggestions,
25+
hasAction: true,
26+
}]),
27+
preferences: {
28+
includeCompletionsForModuleExports: true,
29+
allowIncompleteCompletions: true,
30+
},
31+
});

0 commit comments

Comments
 (0)