Skip to content

Commit 38076df

Browse files
authoredAug 25, 2022
Fix auto import crash due to difference in paths handling (#50419)
1 parent 12eb519 commit 38076df

File tree

5 files changed

+87
-48
lines changed

5 files changed

+87
-48
lines changed
 

‎src/services/codefixes/importFixes.ts

+29-43
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ namespace ts.codefix {
3232
},
3333
fixIds: [importFixId],
3434
getAllCodeActions: context => {
35-
const { sourceFile, program, preferences, host } = context;
36-
const importAdder = createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ true, preferences, host);
35+
const { sourceFile, program, preferences, host, cancellationToken } = context;
36+
const importAdder = createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ true, preferences, host, cancellationToken);
3737
eachDiagnostic(context, errorCodes, diag => importAdder.addImportFromDiagnostic(diag, context));
3838
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, importAdder.writeFixes));
3939
},
@@ -49,8 +49,8 @@ namespace ts.codefix {
4949
writeFixes: (changeTracker: textChanges.ChangeTracker) => void;
5050
}
5151

52-
export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost): ImportAdder {
53-
return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host);
52+
export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder {
53+
return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host, cancellationToken);
5454
}
5555

5656
interface AddToExistingState {
@@ -59,7 +59,7 @@ namespace ts.codefix {
5959
readonly namedImports: ESMap<string, AddAsTypeOnly>;
6060
}
6161

62-
function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost): ImportAdder {
62+
function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder {
6363
const compilerOptions = program.getCompilerOptions();
6464
// Namespace fixes don't conflict, so just build a list.
6565
const addToNamespace: FixUseNamespaceImport[] = [];
@@ -83,9 +83,9 @@ namespace ts.codefix {
8383
const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions));
8484
const checker = program.getTypeChecker();
8585
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
86-
const exportInfo = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, /*isJsxTagName*/ false, host, program, preferences, useAutoImportProvider);
86+
const exportInfo = getAllExportInfoForSymbol(sourceFile, symbol, symbolName, /*isJsxTagName*/ false, program, host, preferences, cancellationToken);
8787
const useRequire = shouldUseRequire(sourceFile, program);
88-
const fix = getImportFixForSymbol(sourceFile, exportInfo, moduleSymbol, program, /*useNamespaceInfo*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
88+
const fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), moduleSymbol, program, /*useNamespaceInfo*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
8989
if (fix) {
9090
addImport({ fix, symbolName, errorIdentifierText: undefined });
9191
}
@@ -345,11 +345,15 @@ namespace ts.codefix {
345345
formatContext: formatting.FormatContext,
346346
position: number,
347347
preferences: UserPreferences,
348+
cancellationToken: CancellationToken,
348349
): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } {
349350
const compilerOptions = program.getCompilerOptions();
351+
350352
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
351-
? [getSymbolExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)]
352-
: getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, isJsxTagName, host, program, preferences, /*useAutoImportProvider*/ true);
353+
? [getSingleExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)]
354+
: getAllExportInfoForSymbol(sourceFile, targetSymbol, symbolName, isJsxTagName, program, host, preferences, cancellationToken);
355+
356+
Debug.assertIsDefined(exportInfos);
353357
const useRequire = shouldUseRequire(sourceFile, program);
354358
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
355359
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, program, { symbolName, position }, isValidTypeOnlyUseSite, useRequire, host, preferences));
@@ -383,7 +387,17 @@ namespace ts.codefix {
383387
return { description, changes, commands };
384388
}
385389

386-
function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo {
390+
function getAllExportInfoForSymbol(importingFile: SourceFile, symbol: Symbol, symbolName: string, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined {
391+
const getChecker = createGetChecker(program, host);
392+
return getExportInfoMap(importingFile, host, program, preferences, cancellationToken)
393+
.search(importingFile.path, preferCapitalized, name => name === symbolName, info => {
394+
if (skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson)) === symbol) {
395+
return info;
396+
}
397+
});
398+
}
399+
400+
function getSingleExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo {
387401
const compilerOptions = program.getCompilerOptions();
388402
const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false);
389403
if (mainProgramInfo) {
@@ -404,38 +418,6 @@ namespace ts.codefix {
404418
}
405419
}
406420

407-
function getAllReExportingModules(importingFile: SourceFile, targetSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, isJsxTagName: boolean, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
408-
const result: SymbolExportInfo[] = [];
409-
const compilerOptions = program.getCompilerOptions();
410-
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
411-
return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host);
412-
});
413-
414-
forEachExternalModuleToImportFrom(program, host, preferences, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
415-
const checker = program.getTypeChecker();
416-
// Don't import from a re-export when looking "up" like to `./index` or `../index`.
417-
if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) {
418-
return;
419-
}
420-
421-
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
422-
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
423-
result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(defaultInfo.symbol, checker).flags, isFromPackageJson });
424-
}
425-
426-
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
427-
if (exported.name === symbolName && checker.getMergedSymbol(skipAlias(exported, checker)) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
428-
result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, targetFlags: skipAlias(exported, checker).flags, isFromPackageJson });
429-
}
430-
}
431-
});
432-
return result;
433-
434-
function isImportable(program: Program, moduleFile: SourceFile | undefined, isFromPackageJson: boolean) {
435-
return !moduleFile || isImportableFile(program, importingFile, moduleFile, preferences, /*packageJsonFilter*/ undefined, getModuleSpecifierResolutionHost(isFromPackageJson), host.getModuleSpecifierCache?.());
436-
}
437-
}
438-
439421
function getImportFixes(
440422
exportInfos: readonly SymbolExportInfo[],
441423
useNamespaceInfo: {
@@ -661,6 +643,10 @@ namespace ts.codefix {
661643
return true;
662644
}
663645

646+
function createGetChecker(program: Program, host: LanguageServiceHost) {
647+
return memoizeOne((isFromPackageJson: boolean) => isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker());
648+
}
649+
664650
function getNewImportFixes(
665651
program: Program,
666652
sourceFile: SourceFile,
@@ -675,7 +661,7 @@ namespace ts.codefix {
675661
const isJs = isSourceFileJS(sourceFile);
676662
const compilerOptions = program.getCompilerOptions();
677663
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
678-
const getChecker = memoizeOne((isFromPackageJson: boolean) => isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker());
664+
const getChecker = createGetChecker(program, host);
679665
const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(getEmitModuleResolutionKind(compilerOptions));
680666
const getModuleSpecifiers = fromCacheOnly
681667
? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false })

‎src/services/completions.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1671,7 +1671,7 @@ namespace ts.Completions {
16711671
}
16721672
case "symbol": {
16731673
const { symbol, location, contextToken, origin, previousToken } = symbolCompletion;
1674-
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source);
1674+
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source, cancellationToken);
16751675
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
16761676
}
16771677
case "literal": {
@@ -1722,6 +1722,7 @@ namespace ts.Completions {
17221722
preferences: UserPreferences,
17231723
data: CompletionEntryData | undefined,
17241724
source: string | undefined,
1725+
cancellationToken: CancellationToken,
17251726
): CodeActionsAndSourceDisplay {
17261727
if (data?.moduleSpecifier) {
17271728
if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) {
@@ -1786,7 +1787,8 @@ namespace ts.Completions {
17861787
program,
17871788
formatContext,
17881789
previousToken && isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position,
1789-
preferences);
1790+
preferences,
1791+
cancellationToken);
17901792
Debug.assert(!data?.moduleSpecifier || moduleSpecifier === data.moduleSpecifier);
17911793
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
17921794
}

‎src/services/exportInfoMap.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ namespace ts {
4949
clear(): void;
5050
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: TypeChecker): void;
5151
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
52-
search(importingFile: Path, preferCapitalized: boolean, matches: (name: string, targetFlags: SymbolFlags) => boolean, action: (info: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean, key: string) => void): void;
52+
search<T>(importingFile: Path, preferCapitalized: boolean, matches: (name: string, targetFlags: SymbolFlags) => boolean, action: (info: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean, key: string) => T | undefined): T | undefined;
5353
releaseSymbols(): void;
5454
isEmpty(): boolean;
5555
/** @returns Whether the change resulted in the cache being cleared */
@@ -160,14 +160,15 @@ namespace ts {
160160
},
161161
search: (importingFile, preferCapitalized, matches, action) => {
162162
if (importingFile !== usableByFileName) return;
163-
exportInfo.forEach((info, key) => {
163+
return forEachEntry(exportInfo, (info, key) => {
164164
const { symbolName, ambientModuleName } = parseKey(key);
165165
const name = preferCapitalized && info[0].capitalizedSymbolName || symbolName;
166166
if (matches(name, info[0].targetFlags)) {
167167
const rehydrated = info.map(rehydrateCachedInfo);
168168
const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName));
169169
if (filtered.length) {
170-
action(filtered, name, !!ambientModuleName, key);
170+
const res = action(filtered, name, !!ambientModuleName, key);
171+
if (res !== undefined) return res;
171172
}
172173
}
173174
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /tsconfig.json
4+
//// {
5+
//// "compilerOptions": {
6+
//// "module": "esnext",
7+
//// "paths": {
8+
//// "@reduxjs/toolkit": ["src/index.ts"],
9+
//// "@internal/*": ["src/*"]
10+
//// }
11+
//// }
12+
//// }
13+
14+
// @Filename: /src/index.ts
15+
//// export { configureStore } from "./configureStore";
16+
17+
// @Filename: /src/configureStore.ts
18+
//// export function configureStore() {}
19+
20+
// @Filename: /src/tests/createAsyncThunk.typetest.ts
21+
//// import {} from "@reduxjs/toolkit";
22+
//// /**/
23+
24+
verify.completions({
25+
marker: "",
26+
includes: [{
27+
name: "configureStore",
28+
source: "@reduxjs/toolkit",
29+
sourceDisplay: "@reduxjs/toolkit",
30+
hasAction: true,
31+
sortText: completion.SortText.AutoImportSuggestions,
32+
}],
33+
preferences: {
34+
allowIncompleteCompletions: true,
35+
includeCompletionsForModuleExports: true,
36+
},
37+
});
38+
39+
verify.applyCodeActionFromCompletion("", {
40+
name: "configureStore",
41+
source: "@reduxjs/toolkit",
42+
data: {
43+
exportName: "configureStore",
44+
fileName: "/src/configureStore.ts",
45+
moduleSpecifier: "@reduxjs/toolkit",
46+
},
47+
description: `Update import from "@reduxjs/toolkit"`,
48+
newFileContent: `import { configureStore } from "@reduxjs/toolkit";\n`,
49+
});

‎tests/cases/fourslash/fourslash.ts

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ declare module ts {
108108
fileName?: string;
109109
ambientModuleName?: string;
110110
isPackageJsonImport?: true;
111+
moduleSpecifier?: string;
111112
exportName: string;
112113
}
113114

0 commit comments

Comments
 (0)
Please sign in to comment.