diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts
index 5f1754c3036..9d34ae78e1c 100644
--- a/src/services/codefixes/importFixes.ts
+++ b/src/services/codefixes/importFixes.ts
@@ -1,3 +1,5 @@
+///
+
/* @internal */
namespace ts.codefix {
export const importFixId = "fixMissingImport";
@@ -380,7 +382,7 @@ 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);
- const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).entries(), ([_, exportInfos]) =>
+ const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, host).entries(), ([_, exportInfos]) =>
getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), program, sourceFile, host, preferences)));
return { fixes, symbolName };
}
@@ -393,6 +395,7 @@ namespace ts.codefix {
sourceFile: SourceFile,
checker: TypeChecker,
program: Program,
+ host: LanguageServiceHost
): ReadonlyMap> {
// 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).
@@ -400,7 +403,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(checker, host, sourceFile, program.getSourceFiles(), moduleSymbol => {
cancellationToken.throwIfCancellationRequested();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, program.getCompilerOptions());
@@ -564,12 +567,59 @@ namespace ts.codefix {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}
- export function forEachExternalModuleToImportFrom(checker: TypeChecker, from: SourceFile, allSourceFiles: ReadonlyArray, cb: (module: Symbol) => void) {
+ export function forEachExternalModuleToImportFrom(checker: TypeChecker, host: LanguageServiceHost, from: SourceFile, allSourceFiles: ReadonlyArray, cb: (module: Symbol) => void) {
+ const packageJsons = findPackageJsons(getDirectoryPath(from.fileName), host);
+ const dependencyIterator = readPackageJsonDependencies();
forEachExternalModule(checker, allSourceFiles, (module, sourceFile) => {
- if (sourceFile === undefined || sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) {
+ if (sourceFile === undefined || sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName) && packageJsonAllowsImporting(sourceFile.fileName)) {
cb(module);
}
});
+
+ let seenDeps: Map | undefined;
+ function packageJsonAllowsImporting(fileName: string): Ternary {
+ const pathComponents = getPathComponents(getDirectoryPath(fileName));
+ const nodeModulesIndex = pathComponents.lastIndexOf("node_modules");
+ if (nodeModulesIndex === -1) {
+ return Ternary.Maybe;
+ }
+ const moduleSpecifier = pathComponents[nodeModulesIndex + 1] === "@types"
+ ? pathComponents[nodeModulesIndex + 2]
+ : pathComponents[nodeModulesIndex + 1];
+ if (!moduleSpecifier) {
+ return Ternary.Maybe;
+ }
+
+ if (seenDeps && seenDeps.has(moduleSpecifier)) {
+ return Ternary.True;
+ }
+
+ let packageName: string;
+ while (packageName = dependencyIterator.next().value) {
+ (seenDeps || (seenDeps = createMap())).set(packageName, true);
+ if (packageName === moduleSpecifier) {
+ return Ternary.True;
+ }
+ }
+ return Ternary.False;
+ }
+
+ type PackageJson = Record | undefined>;
+ function *readPackageJsonDependencies() {
+ const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies"] as const;
+ for (const fileName of packageJsons) {
+ const content = readJson(fileName, { readFile: host.readFile ? host.readFile.bind(host) : sys.readFile }) as PackageJson;
+ for (const key of dependencyKeys) {
+ const dependencyHash = content[key];
+ if (!dependencyHash) {
+ continue;
+ }
+ for (const packageName in dependencyHash) {
+ yield packageName;
+ }
+ }
+ }
+ }
}
function forEachExternalModule(checker: TypeChecker, allSourceFiles: ReadonlyArray, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
diff --git a/src/services/completions.ts b/src/services/completions.ts
index e9a38fb1516..328ba2a119c 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -64,7 +64,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;
}
@@ -407,10 +407,10 @@ 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" };
}
@@ -472,7 +472,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;
@@ -557,8 +557,8 @@ 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;
}
@@ -657,6 +657,7 @@ namespace ts.Completions {
position: number,
preferences: Pick,
detailsEntryId: CompletionEntryIdentifier | undefined,
+ host: LanguageServiceHost
): CompletionData | Request | undefined {
const typeChecker = program.getTypeChecker();
@@ -1151,7 +1152,7 @@ namespace ts.Completions {
}
if (shouldOfferImportCompletions()) {
- getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target!);
+ getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target!, host);
}
filterGlobalCompletion(symbols);
}
@@ -1269,12 +1270,12 @@ namespace ts.Completions {
typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules));
}
- function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
+ function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget, host: LanguageServiceHost): void {
const tokenTextLowerCase = tokenText.toLowerCase();
const seenResolvedModules = createMap();
- codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
+ codefix.forEachExternalModuleToImportFrom(typeChecker, host, sourceFile, program.getSourceFiles(), moduleSymbol => {
// Perf -- ignore other modules if this is a request for details
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
return;
diff --git a/src/services/services.ts b/src/services/services.ts
index fab6f88b779..2f000bc0f2e 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -1453,7 +1453,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 {