Updated: Only auto-import from package.json (#32517)

* Move package.json related utils to utilities

* Add failing test

* Make first test pass

* Don’t filter when there’s no package.json, fix scoped package imports

* Use type acquisition as a heuristic for whether a JS project is using node core

* Make same fix in getCompletionDetails

* Fix re-exporting

* Change JS node core module heuristic to same-file utilization

* Remove unused method

* Remove other unused method

* Remove unused triple-slash ref

* Update comment

* Refactor findAlias to forEachAlias to reduce iterations

* Really fix re-exporting

* Use getModuleSpecifier instead of custom hack

* Fix offering auto imports to paths within node modules

* Rename things and make comments better

* Add another reexport test

* Inline `symbolHasBeenSeen`

* Simplify forEachAlias to findAlias

* Add note that symbols is mutated

* Symbol order doesn’t matter here

* Style nits

* Add test with nested package.jsons

* Fix and add tests for export * re-exports

* Don’t fail when alias isn’t found

* Make some easy optimizations

* Clean up memoization when done

* Remove unnecessary semicolon

* Make getSymbolsFromOtherSourceFileExports pure

* Cache auto imports

* Revert "Cache auto imports"

This reverts commit 8ea4829587.

* Handle merged symbols through cache

* Be safer with symbol declarations, add logging

* Improve cache invalidation for imports and exports

* Check symbol presence first

* Only run cache invalidation logic if there’s something to clear

* Consolidate cache invalidation logic

* Fix reuseProgramStructure test

* Add more logging

* Only clear cache if symbols are different

* Refactor ambient module handling

* Start caching package.json stuff

* Support package.json searching in fourslash

* Move import suggestions cache to Project

* Start making more module specifier work available without having the importing file

* Going to backtrack some from here

* Get rid of dumb cache, fix node core modules stuff

* Start determining changes to a file have invalidated its own auto imports

* Move package.json related utils to utilities

* Add failing test

* Make first test pass

* Don’t filter when there’s no package.json, fix scoped package imports

* Use type acquisition as a heuristic for whether a JS project is using node core

* Make same fix in getCompletionDetails

* Fix re-exporting

* Change JS node core module heuristic to same-file utilization

* Remove unused method

* Remove other unused method

* Remove unused triple-slash ref

* Update comment

* Refactor findAlias to forEachAlias to reduce iterations

* Really fix re-exporting

* Use getModuleSpecifier instead of custom hack

* Fix offering auto imports to paths within node modules

* Rename things and make comments better

* Add another reexport test

* Inline `symbolHasBeenSeen`

* Simplify forEachAlias to findAlias

* Add note that symbols is mutated

* Symbol order doesn’t matter here

* Style nits

* Add test with nested package.jsons

* Fix and add tests for export * re-exports

* Don’t fail when alias isn’t found

* Make some easy optimizations

* Clean up memoization when done

* Remove unnecessary semicolon

* Make getSymbolsFromOtherSourceFileExports pure

* Cache auto imports

* Revert "Cache auto imports"

This reverts commit 8ea4829587.

* Handle merged symbols through cache

* Be safer with symbol declarations, add logging

* Improve cache invalidation for imports and exports

* Check symbol presence first

* Only run cache invalidation logic if there’s something to clear

* Consolidate cache invalidation logic

* Fix reuseProgramStructure test

* Add more logging

* Only clear cache if symbols are different

* Refactor ambient module handling

* Finish(?) sourceFileHasChangedOwnImportSuggestions

* Make package.json info model better

* Fix misplaced paren

* Use file structure cache for package.json detection when possible

* Revert unnecessary changes in moduleSpecifiers

* Revert more unnecessary changes

* Don’t watch package.jsons inside node_modules, fix tests

* Work around declaration emit bug

* Sync submodules?

* Delete unused type

* Add server cache tests

* Fix server fourslash editing

* Fix packageJsonInfo tests

* Add node core modules cache test and fix more fourslash

* Clean up symlink caching

* Improve logging

* Function name doesn’t make any sense anymore

* Move symlinks cache to host

* Fix markFileAsDirty from ScriptInfo

* Mark new Project members internal

* Use Path instead of fileName

* Rename AutoImportSuggestionsCache

* Improve WatchType description

* Remove entries() from packageJsonCache

* Fix path/fileName bug

* Also cache symlinks on Program for benefit of d.ts emit

* Let language service use Program’s symlink cache
This commit is contained in:
Andrew Branch
2019-09-27 13:38:31 -07:00
committed by GitHub
parent 558ece72cb
commit 304fcee09b
42 changed files with 1963 additions and 210 deletions
+173 -9
View File
@@ -284,13 +284,27 @@ namespace ts.codefix {
preferences: UserPreferences,
): readonly (FixAddNewImport | FixUseImportType)[] {
const isJs = isSourceFileJS(sourceFile);
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences, program.redirectTargetsMap)
.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
exportedSymbolIsTypeOnly && isJs ? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.assertDefined(position, "position should be defined") } : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind }));
// Sort to keep the shortest paths first
return sort(choicesForEachExportingModule, (a, b) => a.moduleSpecifier.length - b.moduleSpecifier.length);
exportedSymbolIsTypeOnly && isJs
? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.assertDefined(position, "position should be defined") }
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind }));
// Sort by presence in package.json, then shortest paths first
return sort(choicesForEachExportingModule, (a, b) => {
const allowsImportingA = allowsImportingSpecifier(a.moduleSpecifier);
const allowsImportingB = allowsImportingSpecifier(b.moduleSpecifier);
if (allowsImportingA && !allowsImportingB) {
return -1;
}
if (allowsImportingB && !allowsImportingA) {
return 1;
}
return a.moduleSpecifier.length - b.moduleSpecifier.length;
});
}
function getFixesForAddImport(
@@ -384,7 +398,8 @@ namespace ts.codefix {
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
Debug.assert(symbolName !== InternalSymbolName.Default, "'default' isn't a legal identifier and couldn't occur here");
const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).entries(), ([_, exportInfos]) =>
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, host);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), program, sourceFile, host, preferences)));
return { fixes, symbolName };
}
@@ -397,6 +412,7 @@ namespace ts.codefix {
sourceFile: SourceFile,
checker: TypeChecker,
program: Program,
host: LanguageServiceHost
): ReadonlyMap<readonly SymbolExportInfo[]> {
// For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once.
// Maps symbol id to info for modules providing that symbol (original export + re-exports).
@@ -404,7 +420,7 @@ namespace ts.codefix {
function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind): void {
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker) });
}
forEachExternalModuleToImportFrom(checker, sourceFile, program.getSourceFiles(), moduleSymbol => {
forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, moduleSymbol => {
cancellationToken.throwIfCancellationRequested();
const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, program.getCompilerOptions());
@@ -605,12 +621,37 @@ namespace ts.codefix {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}
export function forEachExternalModuleToImportFrom(checker: TypeChecker, from: SourceFile, allSourceFiles: readonly SourceFile[], cb: (module: Symbol) => void) {
forEachExternalModule(checker, allSourceFiles, (module, sourceFile) => {
if (sourceFile === undefined || sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) {
cb(module);
export function forEachExternalModuleToImportFrom(
program: Program,
host: LanguageServiceHost,
from: SourceFile,
filterByPackageJson: boolean,
cb: (module: Symbol) => void,
) {
let filteredCount = 0;
const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host);
const allSourceFiles = program.getSourceFiles();
forEachExternalModule(program.getTypeChecker(), allSourceFiles, (module, sourceFile) => {
if (sourceFile === undefined) {
if (!packageJson || packageJson.allowsImportingAmbientModule(module, allSourceFiles)) {
cb(module);
}
else if (packageJson) {
filteredCount++;
}
}
else if (sourceFile && sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) {
if (!packageJson || packageJson.allowsImportingSourceFile(sourceFile, allSourceFiles)) {
cb(module);
}
else if (packageJson) {
filteredCount++;
}
}
});
if (host.log) {
host.log(`forEachExternalModuleToImportFrom: filtered out ${filteredCount} modules by package.json contents`);
}
}
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
@@ -664,4 +705,127 @@ namespace ts.codefix {
// Need `|| "_"` to ensure result isn't empty.
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
}
function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost) {
const packageJsons = host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName) || getPackageJsonsVisibleToFile(fromFile.fileName, host);
const dependencyGroups = PackageJsonDependencyGroup.Dependencies | PackageJsonDependencyGroup.DevDependencies | PackageJsonDependencyGroup.OptionalDependencies;
// Mix in `getProbablySymlinks` from Program when host doesn't have it
// in order for non-Project hosts to have a symlinks cache.
const moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost = {
directoryExists: maybeBind(host, host.directoryExists),
fileExists: maybeBind(host, host.fileExists),
getCurrentDirectory: maybeBind(host, host.getCurrentDirectory),
readFile: maybeBind(host, host.readFile),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks) || program.getProbableSymlinks,
};
let usesNodeCoreModules: boolean | undefined;
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier };
function moduleSpecifierIsCoveredByPackageJson(specifier: string) {
const packageName = getNodeModuleRootSpecifier(specifier);
for (const packageJson of packageJsons) {
if (packageJson.has(packageName, dependencyGroups) || packageJson.has(getTypesPackageName(packageName), dependencyGroups)) {
return true;
}
}
return false;
}
function allowsImportingAmbientModule(moduleSymbol: Symbol, allSourceFiles: readonly SourceFile[]): boolean {
if (!packageJsons.length) {
return true;
}
const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile();
const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, allSourceFiles);
if (typeof declaringNodeModuleName === "undefined") {
return true;
}
const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName());
if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName)
|| moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier);
}
function allowsImportingSourceFile(sourceFile: SourceFile, allSourceFiles: readonly SourceFile[]): boolean {
if (!packageJsons.length) {
return true;
}
const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, allSourceFiles);
if (!moduleSpecifier) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
/**
* Use for a specific module specifier that has already been resolved.
* Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve
* the best module specifier for a given module _and_ determine if its importable.
*/
function allowsImportingSpecifier(moduleSpecifier: string) {
if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) {
return true;
}
if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
function isAllowedCoreNodeModulesImport(moduleSpecifier: string) {
// If were in JavaScript, it can be difficult to tell whether the user wants to import
// from Node core modules or not. We can start by seeing if the user is actually using
// any node core modules, as opposed to simply having @types/node accidentally as a
// dependency of a dependency.
if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) {
if (usesNodeCoreModules === undefined) {
usesNodeCoreModules = consumesNodeCoreModules(fromFile);
}
if (usesNodeCoreModules) {
return true;
}
}
return false;
}
function getNodeModulesPackageNameFromFileName(importedFileName: string, allSourceFiles: readonly SourceFile[]): string | undefined {
if (!stringContains(importedFileName, "node_modules")) {
return undefined;
}
const specifier = moduleSpecifiers.getNodeModulesPackageName(
host.getCompilationSettings(),
fromFile.path,
importedFileName,
moduleSpecifierResolutionHost,
allSourceFiles,
program.redirectTargetsMap);
if (!specifier) {
return undefined;
}
// Paths here are not node_modules, so we dont care about them;
// returning anything will trigger a lookup in package.json.
if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) {
return getNodeModuleRootSpecifier(specifier);
}
}
function getNodeModuleRootSpecifier(fullSpecifier: string): string {
const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1);
// Scoped packages
if (startsWith(components[0], "@")) {
return `${components[0]}/${components[1]}`;
}
return components[0];
}
}
}
+242 -38
View File
@@ -50,7 +50,72 @@ namespace ts.Completions {
const enum GlobalsSearch { Continue, Success, Fail }
export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences, triggerCharacter: CompletionsTriggerCharacter | undefined): CompletionInfo | undefined {
export interface AutoImportSuggestion {
symbol: Symbol;
symbolName: string;
skipFilter: boolean;
origin: SymbolOriginInfoExport;
}
export interface ImportSuggestionsForFileCache {
clear(): void;
get(fileName: string, checker: TypeChecker, projectVersion?: string): readonly AutoImportSuggestion[] | undefined;
set(fileName: string, suggestions: readonly AutoImportSuggestion[], projectVersion?: string): void;
isEmpty(): boolean;
}
export function createImportSuggestionsForFileCache(): ImportSuggestionsForFileCache {
let cache: readonly AutoImportSuggestion[] | undefined;
let projectVersion: string | undefined;
let fileName: string | undefined;
return {
isEmpty() {
return !cache;
},
clear: () => {
cache = undefined;
fileName = undefined;
projectVersion = undefined;
},
set: (file, suggestions, version) => {
cache = suggestions;
fileName = file;
if (version) {
projectVersion = version;
}
},
get: (file, checker, version) => {
if (file !== fileName) {
return undefined;
}
if (version) {
return projectVersion === version ? cache : undefined;
}
forEach(cache, suggestion => {
// If the symbol/moduleSymbol was a merged symbol, it will have a new identity
// in the checker, even though the symbols to merge are the same (guaranteed by
// cache invalidation in synchronizeHostData).
if (suggestion.symbol.declarations) {
suggestion.symbol = checker.getMergedSymbol(suggestion.origin.isDefaultExport
? suggestion.symbol.declarations[0].localSymbol || suggestion.symbol.declarations[0].symbol
: suggestion.symbol.declarations[0].symbol);
}
if (suggestion.origin.moduleSymbol.declarations) {
suggestion.origin.moduleSymbol = checker.getMergedSymbol(suggestion.origin.moduleSymbol.declarations[0].symbol);
}
});
return cache;
},
};
}
export function getCompletionsAtPosition(
host: LanguageServiceHost,
program: Program,
log: Log,
sourceFile: SourceFile,
position: number,
preferences: UserPreferences,
triggerCharacter: CompletionsTriggerCharacter | undefined,
): CompletionInfo | undefined {
const typeChecker = program.getTypeChecker();
const compilerOptions = program.getCompilerOptions();
@@ -69,7 +134,7 @@ namespace ts.Completions {
return getLabelCompletionAtPosition(contextToken.parent);
}
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined);
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined, host);
if (!completionData) {
return undefined;
}
@@ -418,10 +483,16 @@ namespace ts.Completions {
previousToken: Node | undefined;
readonly isJsxInitializer: IsJsxInitializer;
}
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
function getSymbolCompletionFromEntryId(
program: Program,
log: Log,
sourceFile: SourceFile,
position: number,
entryId: CompletionEntryIdentifier,
host: LanguageServiceHost,
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } {
const compilerOptions = program.getCompilerOptions();
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host);
if (!completionData) {
return { type: "none" };
}
@@ -442,7 +513,7 @@ namespace ts.Completions {
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind);
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
: undefined;
}) || { type: "none" };
}
@@ -483,7 +554,7 @@ namespace ts.Completions {
}
// Compute all the completion symbols again.
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
switch (symbolCompletion.type) {
case "request": {
const { request } = symbolCompletion;
@@ -568,8 +639,15 @@ namespace ts.Completions {
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
}
export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined {
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
export function getCompletionEntrySymbol(
program: Program,
log: Log,
sourceFile: SourceFile,
position: number,
entryId: CompletionEntryIdentifier,
host: LanguageServiceHost,
): Symbol | undefined {
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
return completion.type === "symbol" ? completion.symbol : undefined;
}
@@ -668,6 +746,7 @@ namespace ts.Completions {
position: number,
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
detailsEntryId: CompletionEntryIdentifier | undefined,
host: LanguageServiceHost,
): CompletionData | Request | undefined {
const typeChecker = program.getTypeChecker();
@@ -886,6 +965,7 @@ namespace ts.Completions {
let symbols: Symbol[] = [];
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
const symbolToSortTextMap: SymbolSortTextMap = [];
const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache();
if (isRightOfDot) {
getTypeScriptMemberSymbols();
@@ -916,7 +996,6 @@ namespace ts.Completions {
}
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined);
@@ -1183,7 +1262,26 @@ namespace ts.Completions {
}
if (shouldOfferImportCompletions()) {
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target!);
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host);
if (!detailsEntryId && importSuggestionsCache) {
importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion());
}
autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => {
if (detailsEntryId) {
if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) {
return;
}
}
else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) {
return;
}
const symbolId = getSymbolId(symbol);
symbols.push(symbol);
symbolToOriginInfoMap[symbolId] = origin;
symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions;
});
}
filterGlobalCompletion(symbols);
}
@@ -1301,12 +1399,77 @@ namespace ts.Completions {
typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules));
}
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
const tokenTextLowerCase = tokenText.toLowerCase();
/**
* Gathers symbols that can be imported from other files, deduplicating along the way. Symbols can be “duplicates”
* if re-exported from another module, e.g. `export { foo } from "./a"`. That syntax creates a fresh symbol, but
* its just an alias to the first, and both have the same name, so we generally want to filter those aliases out,
* if and only if the the first can be imported (it may be excluded due to package.json filtering in
* `codefix.forEachExternalModuleToImportFrom`).
*
* Example. Imagine a chain of node_modules re-exporting one original symbol:
*
* ```js
* node_modules/x/index.js node_modules/y/index.js node_modules/z/index.js
* +-----------------------+ +--------------------------+ +--------------------------+
* | | | | | |
* | export const foo = 0; | <--- | export { foo } from 'x'; | <--- | export { foo } from 'y'; |
* | | | | | |
* +-----------------------+ +--------------------------+ +--------------------------+
* ```
*
* Also imagine three buckets, which well reference soon:
*
* ```md
* | | | | | |
* | **Bucket A** | | **Bucket B** | | **Bucket C** |
* | Symbols to | | Aliases to symbols | | Symbols to return |
* | definitely | | in Buckets A or C | | if nothing better |
* | return | | (dont return these) | | comes along |
* |__________________| |______________________| |___________________|
* ```
*
* We _probably_ want to show `foo` from 'x', but not from 'y' or 'z'. However, if 'x' is not in a package.json, it
* will not appear in a `forEachExternalModuleToImportFrom` iteration. Furthermore, the order of iterations is not
* guaranteed, as it is host-dependent. Therefore, when presented with the symbol `foo` from module 'y' alone, we
* may not be sure whether or not it should go in the list. So, well take the following steps:
*
* 1. Resolve alias `foo` from 'y' to the export declaration in 'x', get the symbol there, and see if that symbol is
* already in Bucket A (symbols we already know will be returned). If it is, put `foo` from 'y' in Bucket B
* (symbols that are aliases to symbols in Bucket A). If its not, put it in Bucket C.
* 2. Next, imagine we see `foo` from module 'z'. Again, we resolve the alias to the nearest export, which is in 'y'.
* At this point, if that nearest export from 'y' is in _any_ of the three buckets, we know the symbol in 'z'
* should never be returned in the final list, so put it in Bucket B.
* 3. Next, imagine we see `foo` from module 'x', the original. Syntactically, it doesnt look like a re-export, so
* we can just check Bucket C to see if we put any aliases to the original in there. If they exist, throw them out.
* Put this symbol in Bucket A.
* 4. After weve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didnt
* occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move
* everything from Bucket C to Bucket A.
*/
function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] {
const cached = importSuggestionsCache && importSuggestionsCache.get(
sourceFile.fileName,
typeChecker,
detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined);
if (cached) {
log("getSymbolsFromOtherSourceFileExports: Using cached list");
return cached;
}
const startTime = timestamp();
log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`);
const seenResolvedModules = createMap<true>();
/** Bucket B */
const aliasesToAlreadyIncludedSymbols = createMap<true>();
/** Bucket C */
const aliasesToReturnIfOriginalsAreMissing = createMap<{ alias: Symbol, moduleSymbol: Symbol }>();
/** Bucket A */
const results: AutoImportSuggestion[] = [];
/** Ids present in `results` for faster lookup */
const resultSymbolIds = createMap<true>();
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, moduleSymbol => {
// Perf -- ignore other modules if this is a request for details
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
return;
@@ -1318,42 +1481,74 @@ namespace ts.Completions {
return;
}
// Don't add another completion for `export =` of a symbol that's already global.
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
if (resolvedModuleSymbol !== moduleSymbol &&
// Don't add another completion for `export =` of a symbol that's already global.
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
every(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator)) {
symbols.push(resolvedModuleSymbol);
symbolToSortTextMap[getSymbolId(resolvedModuleSymbol)] = SortText.AutoImportSuggestions;
symbolToOriginInfoMap[getSymbolId(resolvedModuleSymbol)] = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport: false };
pushSymbol(resolvedModuleSymbol, moduleSymbol, /*skipFilter*/ true);
}
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
// Don't add a completion for a re-export, only for the original.
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
// This is just to avoid adding duplicate completion entries.
//
// If `symbol.parent !== ...`, this is an `export * from "foo"` re-export. Those don't create new symbols.
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol
|| some(symbol.declarations, d =>
// If `!!d.name.originalKeywordKind`, this is `export { _break as break };` -- skip this and prefer the keyword completion.
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
isExportSpecifier(d) && (d.propertyName ? isIdentifierANonContextualKeyword(d.name) : !!d.parent.parent.moduleSpecifier))) {
for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
// If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion.
if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) {
continue;
}
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
if (isDefaultExport) {
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
const symbolId = getSymbolId(symbol).toString();
// If `symbol.parent !== moduleSymbol`, this is an `export * from "foo"` re-export. Those don't create new symbols.
const isExportStarFromReExport = typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol;
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
if (isExportStarFromReExport || some(symbol.declarations, d => isExportSpecifier(d) && !d.propertyName && !!d.parent.parent.moduleSpecifier)) {
// Walk the export chain back one module (step 1 or 2 in diagrammed example).
// Or, in the case of `export * from "foo"`, `symbol` already points to the original export, so just use that.
const nearestExportSymbol = isExportStarFromReExport ? symbol : getNearestExportSymbol(symbol);
if (!nearestExportSymbol) continue;
const nearestExportSymbolId = getSymbolId(nearestExportSymbol).toString();
const symbolHasBeenSeen = resultSymbolIds.has(nearestExportSymbolId) || aliasesToAlreadyIncludedSymbols.has(nearestExportSymbolId);
if (!symbolHasBeenSeen) {
aliasesToReturnIfOriginalsAreMissing.set(nearestExportSymbolId, { alias: symbol, moduleSymbol });
aliasesToAlreadyIncludedSymbols.set(symbolId, true);
}
else {
// Perf - we know this symbol is an alias to one thats already covered in `symbols`, so store it here
// in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id.
addToSeen(aliasesToAlreadyIncludedSymbols, symbolId);
}
}
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
symbols.push(symbol);
symbolToSortTextMap[getSymbolId(symbol)] = SortText.AutoImportSuggestions;
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
else {
// This is not a re-export, so see if we have any aliases pending and remove them (step 3 in diagrammed example)
aliasesToReturnIfOriginalsAreMissing.delete(symbolId);
pushSymbol(symbol, moduleSymbol);
}
}
});
// By this point, any potential duplicates that were actually duplicates have been
// removed, so the rest need to be added. (Step 4 in diagrammed example)
aliasesToReturnIfOriginalsAreMissing.forEach(({ alias, moduleSymbol }) => pushSymbol(alias, moduleSymbol));
log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`);
return results;
function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, skipFilter = false) {
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
if (isDefaultExport) {
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
}
addToSeen(resultSymbolIds, getSymbolId(symbol));
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
results.push({
symbol,
symbolName: getSymbolName(symbol, origin, target),
origin,
skipFilter,
});
}
}
function getNearestExportSymbol(fromSymbol: Symbol) {
return findAlias(typeChecker, fromSymbol, alias => {
return some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol);
});
}
/**
@@ -2331,4 +2526,13 @@ namespace ts.Completions {
function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean {
return nodeIsMissing(left);
}
function findAlias(typeChecker: TypeChecker, symbol: Symbol, predicate: (symbol: Symbol) => boolean): Symbol | undefined {
let currentAlias: Symbol | undefined = symbol;
while (currentAlias.flags & SymbolFlags.Alias && (currentAlias = typeChecker.getImmediateAliasedSymbol(currentAlias))) {
if (predicate(currentAlias)) {
return currentAlias;
}
}
}
}
+1 -1
View File
@@ -1462,7 +1462,7 @@ namespace ts {
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol | undefined {
synchronizeHostData();
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source });
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host);
}
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
-49
View File
@@ -624,30 +624,6 @@ namespace ts.Completions.StringCompletions {
}
}
function findPackageJsons(directory: string, host: LanguageServiceHost): string[] {
const paths: string[] = [];
forEachAncestorDirectory(directory, ancestor => {
const currentConfigPath = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
if (!currentConfigPath) {
return true; // break out
}
paths.push(currentConfigPath);
});
return paths;
}
function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
let packageJson: string | undefined;
forEachAncestorDirectory(directory, ancestor => {
if (ancestor === "node_modules") return true;
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
if (packageJson) {
return true; // break out
}
});
return packageJson;
}
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] {
if (!host.readFile || !host.fileExists) return emptyArray;
@@ -703,31 +679,6 @@ namespace ts.Completions.StringCompletions {
const nodeModulesDependencyKeys: readonly string[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] {
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || [];
}
function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] {
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
}
function tryFileExists(host: LanguageServiceHost, path: string): boolean {
return tryIOAndConsumeErrors(host, host.fileExists, path);
}
function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false;
}
function tryIOAndConsumeErrors<T>(host: LanguageServiceHost, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) {
return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args));
}
function tryAndIgnoreErrors<T>(cb: () => T): T | undefined {
try { return cb(); }
catch { return undefined; }
}
function containsSlash(fragment: string) {
return stringContains(fragment, directorySeparator);
}
+25 -2
View File
@@ -171,10 +171,30 @@ namespace ts {
packageName: string;
}
/* @internal */
export const enum PackageJsonDependencyGroup {
Dependencies = 1 << 0,
DevDependencies = 1 << 1,
PeerDependencies = 1 << 2,
OptionalDependencies = 1 << 3,
All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies,
}
/* @internal */
export interface PackageJsonInfo {
fileName: string;
dependencies?: Map<string>;
devDependencies?: Map<string>;
peerDependencies?: Map<string>;
optionalDependencies?: Map<string>;
get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined;
has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean;
}
//
// Public interface of the host of a language service instance.
//
export interface LanguageServiceHost extends GetEffectiveTypeRootsHost {
export interface LanguageServiceHost extends ModuleSpecifierResolutionHost {
getCompilationSettings(): CompilerOptions;
getNewLine?(): string;
getProjectVersion?(): string;
@@ -190,7 +210,6 @@ namespace ts {
log?(s: string): void;
trace?(s: string): void;
error?(s: string): void;
useCaseSensitiveFileNames?(): boolean;
/*
* LS host can optionally implement these methods to support completions for module specifiers.
@@ -239,6 +258,10 @@ namespace ts {
/* @internal */
getSourceFileLike?(fileName: string): SourceFileLike | undefined;
/* @internal */
getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[];
/* @internal */
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;
/* @internal */
setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void;
/* @internal */
useSourceOfProjectReferenceRedirect?(): boolean;
+141
View File
@@ -2105,4 +2105,145 @@ namespace ts {
// If even 2/5 places have a semicolon, the user probably wants semicolons
return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve;
}
export function tryGetDirectories(host: Pick<LanguageServiceHost, "getDirectories">, directoryName: string): string[] {
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || [];
}
export function tryReadDirectory(host: Pick<LanguageServiceHost, "readDirectory">, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] {
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
}
export function tryFileExists(host: Pick<LanguageServiceHost, "fileExists">, path: string): boolean {
return tryIOAndConsumeErrors(host, host.fileExists, path);
}
export function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false;
}
export function tryAndIgnoreErrors<T>(cb: () => T): T | undefined {
try { return cb(); }
catch { return undefined; }
}
export function tryIOAndConsumeErrors<T>(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) {
return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args));
}
export function findPackageJsons(startDirectory: string, host: Pick<LanguageServiceHost, "fileExists">, stopDirectory?: string): string[] {
const paths: string[] = [];
forEachAncestorDirectory(startDirectory, ancestor => {
if (ancestor === stopDirectory) {
return true;
}
const currentConfigPath = combinePaths(ancestor, "package.json");
if (tryFileExists(host, currentConfigPath)) {
paths.push(currentConfigPath);
}
});
return paths;
}
export function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
let packageJson: string | undefined;
forEachAncestorDirectory(directory, ancestor => {
if (ancestor === "node_modules") return true;
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
if (packageJson) {
return true; // break out
}
});
return packageJson;
}
export function getPackageJsonsVisibleToFile(fileName: string, host: LanguageServiceHost): readonly PackageJsonInfo[] {
if (!host.fileExists) {
return [];
}
const packageJsons: PackageJsonInfo[] = [];
forEachAncestorDirectory(getDirectoryPath(fileName), ancestor => {
const packageJsonFileName = combinePaths(ancestor, "package.json");
if (host.fileExists!(packageJsonFileName)) {
const info = createPackageJsonInfo(packageJsonFileName, host);
if (info) {
packageJsons.push(info);
}
}
});
return packageJsons;
}
export function createPackageJsonInfo(fileName: string, host: LanguageServiceHost): PackageJsonInfo | undefined {
if (!host.readFile) {
return undefined;
}
type PackageJsonRaw = Record<typeof dependencyKeys[number], Record<string, string> | undefined>;
const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] as const;
const stringContent = host.readFile(fileName);
const content = stringContent && tryParseJson(stringContent) as PackageJsonRaw;
if (!content) {
return undefined;
}
const info: Pick<PackageJsonInfo, typeof dependencyKeys[number]> = {};
for (const key of dependencyKeys) {
const dependencies = content[key];
if (!dependencies) {
continue;
}
const dependencyMap = createMap<string>();
for (const packageName in dependencies) {
dependencyMap.set(packageName, dependencies[packageName]);
}
info[key] = dependencyMap;
}
const dependencyGroups = [
[PackageJsonDependencyGroup.Dependencies, info.dependencies],
[PackageJsonDependencyGroup.DevDependencies, info.devDependencies],
[PackageJsonDependencyGroup.OptionalDependencies, info.optionalDependencies],
[PackageJsonDependencyGroup.PeerDependencies, info.peerDependencies],
] as const;
return {
...info,
fileName,
get,
has(dependencyName, inGroups) {
return !!get(dependencyName, inGroups);
},
};
function get(dependencyName: string, inGroups = PackageJsonDependencyGroup.All) {
for (const [group, deps] of dependencyGroups) {
if (deps && (inGroups & group)) {
const dep = deps.get(dependencyName);
if (dep !== undefined) {
return dep;
}
}
}
}
}
function tryParseJson(text: string) {
try {
return JSON.parse(text);
}
catch {
return undefined;
}
}
export function consumesNodeCoreModules(sourceFile: SourceFile): boolean {
return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text));
}
export function isInsideNodeModules(fileOrDirectory: string): boolean {
return contains(getPathComponents(fileOrDirectory), "node_modules");
}
}