Skip to content

Commit

Permalink
A-ha moment
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch committed Oct 8, 2021
1 parent 20676c7 commit fef7e8c
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 48 deletions.
53 changes: 33 additions & 20 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ namespace ts.Completions {
moduleSymbol: Symbol;
isDefaultExport: boolean;
exportName: string;
exportMapKey: string;
}

interface SymbolOriginInfoResolvedExport extends SymbolOriginInfoExport {
Expand Down Expand Up @@ -308,9 +309,6 @@ namespace ts.Completions {

const lowerCaseTokenText = location.text.toLowerCase();
const exportMap = getExportInfoMap(file, host, program, cancellationToken);
const checker = program.getTypeChecker();
const autoImportProvider = host.getPackageJsonAutoImportProvider?.();
const autoImportProviderChecker = autoImportProvider?.getTypeChecker();
const newEntries = resolvingModuleSpecifiers(
"continuePreviousIncompleteResponse",
host,
Expand All @@ -329,13 +327,8 @@ namespace ts.Completions {
return undefined;
}

const { symbol, origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
const info = exportMap.get(
file.path,
entry.name,
symbol,
origin.moduleSymbol.name,
origin.isFromPackageJson ? autoImportProviderChecker! : checker);
const { origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
const info = exportMap.get(file.path, entry.data.exportMapKey);

const result = info && context.tryResolve(info, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name)));
if (!result) return entry;
Expand Down Expand Up @@ -763,6 +756,7 @@ namespace ts.Completions {
function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined {
return {
exportName: origin.exportName,
exportMapKey: origin.exportMapKey,
fileName: origin.fileName,
ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name),
isPackageJsonImport: origin.isFromPackageJson ? true : undefined,
Expand Down Expand Up @@ -1762,19 +1756,36 @@ namespace ts.Completions {
const index = symbols.length;
symbols.push(firstAccessibleSymbol);
const moduleSymbol = firstAccessibleSymbol.parent;
if (!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)) {
if (!moduleSymbol ||
!isExternalModuleSymbol(moduleSymbol) ||
typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol
) {
symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) };
}
else {
const origin: SymbolOriginInfoExport = {
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport),
const fileName = isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? getSourceFileOfModule(moduleSymbol)?.fileName : undefined;
const { moduleSpecifier } = codefix.getModuleSpecifierForBestExportInfo([{
exportKind: ExportKind.Named,
moduleFileName: fileName,
isFromPackageJson: false,
moduleSymbol,
isDefaultExport: false,
symbolName: firstAccessibleSymbol.name,
exportName: firstAccessibleSymbol.name,
fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.valueDeclaration, isSourceFile).fileName : undefined,
};
symbolToOriginInfoMap[index] = origin;
symbol: firstAccessibleSymbol,
targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags,
}], sourceFile, program, host, preferences) || {};

if (moduleSpecifier) {
const origin: SymbolOriginInfoResolvedExport = {
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport),
moduleSymbol,
isDefaultExport: false,
symbolName: firstAccessibleSymbol.name,
exportName: firstAccessibleSymbol.name,
fileName,
moduleSpecifier,
exportMapKey: "", // gross, but this will never be inspected since it includes the module specifier already
};
symbolToOriginInfoMap[index] = origin;
}
}
}
else if (preferences.includeCompletionsWithInsertText) {
Expand Down Expand Up @@ -2034,7 +2045,7 @@ namespace ts.Completions {
preferences,
!!importCompletionNode,
context => {
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => {
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule, exportMapKey) => {
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
// `targetFlags` should be the same for each `info`
Expand All @@ -2056,6 +2067,7 @@ namespace ts.Completions {
kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export,
moduleSpecifier,
symbolName,
exportMapKey,
exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name,
fileName: exportInfo.moduleFileName,
isDefaultExport,
Expand Down Expand Up @@ -2997,6 +3009,7 @@ namespace ts.Completions {
symbolName: name,
isDefaultExport,
exportName: data.exportName,
exportMapKey: data.exportMapKey,
fileName: data.fileName,
isFromPackageJson: !!data.isPackageJsonImport,
}
Expand Down
40 changes: 12 additions & 28 deletions src/services/exportInfoMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ namespace ts {
isUsableByFile(importingFile: Path): boolean;
clear(): void;
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly SymbolExportInfo[] | undefined;
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void;
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean, key: string) => void): void;
releaseSymbols(): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
Expand Down Expand Up @@ -87,34 +87,35 @@ namespace ts {
: getNameForExportedSymbol(namedSymbol, scriptTarget);
const moduleName = stripQuotes(moduleSymbol.name);
const id = exportInfoId++;
const target = skipAlias(symbol, checker);
const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol;
const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol;
if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]);

exportInfo.add(key(importedName, symbol, moduleName, checker), {
exportInfo.add(key(importedName, symbol, isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), {
id,
symbolTableKey,
symbolName: importedName,
moduleName,
moduleFile,
moduleFileName: moduleFile?.fileName,
exportKind,
targetFlags: skipAlias(symbol, checker).flags,
targetFlags: target.flags,
isFromPackageJson,
symbol: storedSymbol,
moduleSymbol: storedModuleSymbol,
});
},
get: (importingFile, importedName, symbol, moduleName, checker) => {
get: (importingFile, key) => {
if (importingFile !== usableByFileName) return;
const result = exportInfo.get(key(importedName, symbol, moduleName, checker));
const result = exportInfo.get(key);
return result?.map(rehydrateCachedInfo);
},
forEach: (importingFile, action) => {
if (importingFile !== usableByFileName) return;
exportInfo.forEach((info, key) => {
const { symbolName, ambientModuleName } = parseKey(key);
action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName);
action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName, key);
});
},
releaseSymbols: () => {
Expand Down Expand Up @@ -183,35 +184,18 @@ namespace ts {
};
}

function key(importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker) {
const unquoted = stripQuotes(moduleName);
const moduleKey = isExternalModuleNameRelative(unquoted) ? "/" : unquoted;
return `${importedName}|${createSymbolKey(skipAlias(symbol, checker), unquoted)}|${moduleKey}`;
function key(importedName: string, symbol: Symbol, ambientModuleName: string | undefined, checker: TypeChecker): string {
const moduleKey = ambientModuleName || "";
return `${importedName}|${getSymbolId(skipAlias(symbol, checker))}|${moduleKey}`;
}

function parseKey(key: string) {
const symbolName = key.substring(0, key.indexOf("|"));
const moduleKey = key.substring(key.lastIndexOf("|") + 1);
const ambientModuleName = moduleKey === "/" ? undefined : moduleKey;
const ambientModuleName = moduleKey === "" ? undefined : moduleKey;
return { symbolName, ambientModuleName };
}

function createSymbolKey(symbol: Symbol, sourceModuleName: string) {
let key = symbol.name;
let seenModule = false;
while (symbol.parent) {
seenModule = isExternalModuleSymbol(symbol.parent);
key += `,${symbol.parent.name}`;
symbol = symbol.parent;
}
if (!seenModule) {
const decl = symbol.declarations?.[0];
const fileName = decl && getSourceFileOfNode(decl).fileName;
key += fileName || sourceModuleName;
}
return key;
}

function fileIsGlobalOnly(file: SourceFile) {
return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames;
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,8 @@ namespace ts {
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
/** The key in the `ExportMapCache` where the completion entry's `SymbolExportInfo[]` is found */
exportMapKey: string;
/**
* Set for auto imports with eagerly resolved module specifiers.
*/
Expand Down
33 changes: 33 additions & 0 deletions tests/cases/fourslash/completionsImport_reexportTransient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// <reference path="fourslash.ts" />

// @esModuleInterop: true

// @Filename: /transient.d.ts
//// declare const map: { [K in "one"]: number };
//// export = map;

// @Filename: /r1.ts
//// export { one } from "./transient";

// @Filename: /r2.ts
//// export { one } from "./r1";

// @Filename: /index.ts
//// one/**/

goTo.marker("");

verify.completions({
marker: "",
exact: completion.globalsPlus([{
name: "one",
source: "./transient",
sourceDisplay: "./transient",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions,
}]),
preferences: {
includeCompletionsForModuleExports: true,
allowIncompleteCompletions: true,
}
});

0 comments on commit fef7e8c

Please sign in to comment.