From 52a0a00a161e9be67b8612ed53f9f7444b437dca Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 11 Jun 2019 13:42:27 -0700 Subject: [PATCH] Make first test pass --- src/services/codefixes/importFixes.ts | 58 +++++++++++++++++++++++++-- src/services/completions.ts | 19 ++++----- src/services/services.ts | 2 +- 3 files changed, 65 insertions(+), 14 deletions(-) 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 {