diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3203e10f370e7..0d3bad879deca 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7468,7 +7468,7 @@ namespace ts { export function getDirectoryPath(path: Path): Path; /** * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` - * except that we support URL's as well. + * except that we support URLs as well. * * ```ts * getDirectoryPath("/path/to/file.ext") === "/path/to" diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 5d5338db1f433..56cc2b65508b2 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -798,7 +798,7 @@ namespace FourSlash { const name = typeof include === "string" ? include : include.name; const found = nameToEntries.get(name); if (!found) throw this.raiseError(`No completion ${name} found`); - assert(found.length === 1); // Must use 'exact' for multiple completions with same name + assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`); this.verifyCompletionEntry(ts.first(found), include); } } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 8006d1a0cfd04..2fcc8cf5ccbe6 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -283,13 +283,25 @@ namespace ts.codefix { preferences: UserPreferences, ): ReadonlyArray { const isJs = isSourceFileJS(sourceFile); + const { allowsImporting } = createLazyPackageJsonDependencyReader(sourceFile, 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) } : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind })); - // Sort to keep the shortest paths first - return sort(choicesForEachExportingModule, (a, b) => a.moduleSpecifier.length - b.moduleSpecifier.length); + + // Sort by presence in package.json, then shortest paths first + return sort(choicesForEachExportingModule, (a, b) => { + const allowsImportingA = allowsImporting(a.moduleSpecifier); + const allowsImportingB = allowsImporting(b.moduleSpecifier); + if (allowsImportingA && !allowsImportingB) { + return -1; + } + if (allowsImportingB && !allowsImportingA) { + return 1; + } + return a.moduleSpecifier.length - b.moduleSpecifier.length; + }); } function getFixesForAddImport( @@ -380,7 +392,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); - const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).entries(), ([_, exportInfos]) => + const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, preferences, host); + const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) => getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), program, sourceFile, host, preferences))); return { fixes, symbolName }; } @@ -393,6 +406,8 @@ namespace ts.codefix { sourceFile: SourceFile, checker: TypeChecker, program: Program, + preferences: UserPreferences, + 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 +415,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, preferences, program.redirectTargetsMap, sourceFile, program.getSourceFiles(), moduleSymbol => { cancellationToken.throwIfCancellationRequested(); const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, program.getCompilerOptions()); @@ -561,12 +576,44 @@ 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, preferences: UserPreferences, redirectTargetsMap: RedirectTargetsMap, from: SourceFile, allSourceFiles: ReadonlyArray, cb: (module: Symbol) => void) { + const { allowsImporting } = createLazyPackageJsonDependencyReader(from, host); + const compilerOptions = host.getCompilationSettings(); + const getCanonicalFileName = hostGetCanonicalFileName(host); forEachExternalModule(checker, allSourceFiles, (module, sourceFile) => { - if (sourceFile === undefined || sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) { + if (sourceFile === undefined && allowsImporting(stripQuotes(module.getName()))) { cb(module); } + else if (sourceFile && sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) { + const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName); + if (!moduleSpecifier || allowsImporting(moduleSpecifier)) { + cb(module); + } + } }); + + function getNodeModulesPackageNameFromFileName(importedFileName: string): string | undefined { + const specifier = moduleSpecifiers.getModuleSpecifier( + compilerOptions, + from, + toPath(from.fileName, /*basePath*/ undefined, getCanonicalFileName), + importedFileName, + host, + allSourceFiles, + preferences, + redirectTargetsMap); + + // Paths here are not node_modules, so we don’t care about them; + // returning anything will trigger a lookup in package.json. + if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) { + const components = getPathComponents(getPackageNameFromTypesPackageName(specifier)).slice(1); + // Scoped packages + if (startsWith(components[0], "@")) { + return `${components[0]}/${components[1]}`; + } + return components[0]; + } + } } function forEachExternalModule(checker: TypeChecker, allSourceFiles: ReadonlyArray, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { @@ -620,4 +667,69 @@ namespace ts.codefix { // Need `|| "_"` to ensure result isn't empty. return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`; } + + function createLazyPackageJsonDependencyReader(fromFile: SourceFile, host: LanguageServiceHost) { + const packageJsonPaths = findPackageJsons(getDirectoryPath(fromFile.fileName), host); + const dependencyIterator = readPackageJsonDependencies(host, packageJsonPaths); + let seenDeps: Map | undefined; + let usesNodeCoreModules: boolean | undefined; + return { allowsImporting }; + + function containsDependency(dependency: string) { + if ((seenDeps || (seenDeps = createMap())).has(dependency)) { + return true; + } + let packageName: string | void; + while (packageName = dependencyIterator.next().value) { + seenDeps.set(packageName, true); + if (packageName === dependency) { + return true; + } + } + return false; + } + + function allowsImporting(moduleSpecifier: string): boolean { + if (!packageJsonPaths.length) { + return true; + } + + // If we’re 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 containsDependency(moduleSpecifier) + || containsDependency(getTypesPackageName(moduleSpecifier)); + } + } + + function *readPackageJsonDependencies(host: LanguageServiceHost, packageJsonPaths: string[]) { + type PackageJson = Record | undefined>; + const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies"] as const; + for (const fileName of packageJsonPaths) { + const content = readJson(fileName, { readFile: host.readFile ? host.readFile.bind(host) : sys.readFile }) as PackageJson; + for (const key of dependencyKeys) { + const dependencies = content[key]; + if (!dependencies) { + continue; + } + for (const packageName in dependencies) { + yield packageName; + } + } + } + } + + function consumesNodeCoreModules(sourceFile: SourceFile): boolean { + return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text)); + } } diff --git a/src/services/completions.ts b/src/services/completions.ts index 6d1d48b22b828..cf707c642ed36 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -63,7 +63,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; } @@ -406,10 +406,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" }; } @@ -471,7 +471,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; @@ -556,8 +556,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; } @@ -656,6 +656,7 @@ namespace ts.Completions { position: number, preferences: Pick, detailsEntryId: CompletionEntryIdentifier | undefined, + host: LanguageServiceHost ): CompletionData | Request | undefined { const typeChecker = program.getTypeChecker(); @@ -1148,7 +1149,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); } @@ -1254,12 +1255,64 @@ namespace ts.Completions { typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules)); } - function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void { + /** + * 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 + * it’s 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 we’ll 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 | | (don’t 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, we’ll 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 it’s 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 doesn’t 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 we’ve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didn’t + * 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. + * + * Note: Bucket A is passed in as the parameter `symbols` and mutated. + */ + function getSymbolsFromOtherSourceFileExports(/** Bucket A */ symbols: Symbol[], tokenText: string, target: ScriptTarget, host: LanguageServiceHost): void { const tokenTextLowerCase = tokenText.toLowerCase(); - const seenResolvedModules = createMap(); + /** Bucket B */ + const aliasesToAlreadyIncludedSymbols = createMap(); + /** Bucket C */ + const aliasesToReturnIfOriginalsAreMissing = createMap<{ alias: Symbol, moduleSymbol: Symbol }>(); - codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => { + codefix.forEachExternalModuleToImportFrom(typeChecker, host, preferences, program.redirectTargetsMap, 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; @@ -1280,33 +1333,59 @@ namespace ts.Completions { symbolToOriginInfoMap[getSymbolId(resolvedModuleSymbol)] = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport: false }; } - 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; + // 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 nearestExportSymbolId = getSymbolId(isExportStarFromReExport ? symbol : Debug.assertDefined(getNearestExportSymbol(symbol))); + const symbolHasBeenSeen = !!symbolToOriginInfoMap[nearestExportSymbolId] || aliasesToAlreadyIncludedSymbols.has(nearestExportSymbolId.toString()); + if (!symbolHasBeenSeen) { + aliasesToReturnIfOriginalsAreMissing.set(nearestExportSymbolId.toString(), { alias: symbol, moduleSymbol }); + aliasesToAlreadyIncludedSymbols.set(getSymbolId(symbol).toString(), true); + } + else { + // Perf - we know this symbol is an alias to one that’s 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, getSymbolId(symbol)); + } } - - 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(getSymbolId(symbol).toString()); + 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)); + + function pushSymbol(symbol: Symbol, moduleSymbol: Symbol) { + const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; + if (isDefaultExport) { + symbol = getLocalSymbolForExportDefault(symbol) || symbol; + } + 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; + } + } + } + + function getNearestExportSymbol(fromSymbol: Symbol) { + return findAlias(typeChecker, fromSymbol, alias => { + return some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol); + }); } /** @@ -2216,4 +2295,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; + } + } + } } diff --git a/src/services/services.ts b/src/services/services.ts index fab6f88b779e0..2f000bc0f2e95 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 { diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index b287ebdb406a9..58195b5cb3c50 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -627,30 +627,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): ReadonlyArray { if (!host.readFile || !host.fileExists) return emptyArray; @@ -706,31 +682,6 @@ namespace ts.Completions.StringCompletions { const nodeModulesDependencyKeys: ReadonlyArray = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; - function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] { - return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; - } - - function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): ReadonlyArray { - 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(host: LanguageServiceHost, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { - return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); - } - - function tryAndIgnoreErrors(cb: () => T): T | undefined { - try { return cb(); } - catch { return undefined; } - } - function containsSlash(fragment: string) { return stringContains(fragment, directorySeparator); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 44125fea6d05c..cbadfbc9b53a8 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2038,4 +2038,53 @@ namespace ts { // If even 2/5 places have a semicolon, the user probably wants semicolons return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve; } + + export function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] { + return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; + } + + export function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): ReadonlyArray { + return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray; + } + + export function tryFileExists(host: LanguageServiceHost, 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(cb: () => T): T | undefined { + try { return cb(); } + catch { return undefined; } + } + + export function tryIOAndConsumeErrors(host: LanguageServiceHost, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { + return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); + } + + export 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; + } + + 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; + } } diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesImplicit.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesImplicit.ts new file mode 100644 index 0000000000000..539a9cc8ff6c2 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesImplicit.ts @@ -0,0 +1,44 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "react": "*" +//// } +////} + +//@Filename: /node_modules/@types/react/index.d.ts +////export declare var React: any; + +//@Filename: /node_modules/@types/react/package.json +////{ +//// "name": "@types/react" +////} + +//@Filename: /node_modules/@types/fake-react/index.d.ts +////export declare var ReactFake: any; + +//@Filename: /node_modules/@types/fake-react/package.json +////{ +//// "name": "@types/fake-react" +////} + +//@Filename: /src/index.ts +////const x = Re/**/ + +verify.completions({ + marker: test.marker(""), + isNewIdentifierLocation: true, + includes: { + name: "React", + hasAction: true, + source: "/node_modules/@types/react/index", + sortText: completion.SortText.AutoImportSuggestions + }, + excludes: "ReactFake", + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesOnly.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesOnly.ts new file mode 100644 index 0000000000000..b0d2c01e3dbe9 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_@typesOnly.ts @@ -0,0 +1,44 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "devDependencies": { +//// "@types/react": "*" +//// } +////} + +//@Filename: /node_modules/@types/react/index.d.ts +////export declare var React: any; + +//@Filename: /node_modules/@types/react/package.json +////{ +//// "name": "@types/react" +////} + +//@Filename: /node_modules/@types/fake-react/index.d.ts +////export declare var ReactFake: any; + +//@Filename: /node_modules/@types/fake-react/package.json +////{ +//// "name": "@types/fake-react" +////} + +//@Filename: /src/index.ts +////const x = Re/**/ + +verify.completions({ + marker: test.marker(""), + isNewIdentifierLocation: true, + includes: { + name: "React", + hasAction: true, + source: "/node_modules/@types/react/index", + sortText: completion.SortText.AutoImportSuggestions + }, + excludes: "ReactFake", + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_ambient.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_ambient.ts new file mode 100644 index 0000000000000..3dcb9eb66904f --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_ambient.ts @@ -0,0 +1,30 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// } +////} + +//@Filename: /node_modules/@types/node/timers.d.ts +////declare module "timers" { +//// function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timeout; +////} + +//@Filename: /node_modules/@types/node/package.json +////{ +//// "name": "@types/node", +////} + +//@Filename: /src/index.ts +////setTimeo/**/ + +verify.completions({ + marker: test.marker(""), + exact: completion.globals, + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_direct.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_direct.ts new file mode 100644 index 0000000000000..aa7845daed3d1 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_direct.ts @@ -0,0 +1,46 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "react": "*" +//// } +////} + +//@Filename: /node_modules/react/index.d.ts +////export declare var React: any; + +//@Filename: /node_modules/react/package.json +////{ +//// "name": "react", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/fake-react/index.d.ts +////export declare var ReactFake: any; + +//@Filename: /node_modules/fake-react/package.json +////{ +//// "name": "fake-react", +//// "types": "./index.d.ts" +////} + +//@Filename: /src/index.ts +////const x = Re/**/ + +verify.completions({ + marker: test.marker(""), + isNewIdentifierLocation: true, + includes: { + name: "React", + hasAction: true, + source: "/node_modules/react/index", + sortText: completion.SortText.AutoImportSuggestions + }, + excludes: "ReactFake", + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_nested.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_nested.ts new file mode 100644 index 0000000000000..e940c43e32c6d --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_nested.ts @@ -0,0 +1,66 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "react": "*" +//// } +////} + +//@Filename: /node_modules/react/index.d.ts +////export declare var React: any; + +//@Filename: /node_modules/react/package.json +////{ +//// "name": "react", +//// "types": "./index.d.ts" +////} + +//@Filename: /dir/package.json +////{ +//// "dependencies": { +//// "redux": "*" +//// } +////} + +//@Filename: /dir/node_modules/redux/package.json +////{ +//// "name": "redux", +//// "types": "./index.d.ts" +////} + +//@Filename: /dir/node_modules/redux/index.d.ts +////export declare var Redux: any; + +//@Filename: /dir/index.ts +////const x = Re/**/ + +verify.completions({ + marker: test.marker(""), + isNewIdentifierLocation: true, + includes: { + name: "React", + hasAction: true, + source: "/node_modules/react/index", + sortText: completion.SortText.AutoImportSuggestions + }, + preferences: { + includeCompletionsForModuleExports: true + } +}); + +verify.completions({ + marker: test.marker(""), + isNewIdentifierLocation: true, + includes: { + name: "Redux", + hasAction: true, + source: "/dir/node_modules/redux/index", + sortText: completion.SortText.AutoImportSuggestions + }, + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport.ts new file mode 100644 index 0000000000000..8e17a3c3a449b --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport.ts @@ -0,0 +1,58 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "@emotion/core": "*" +//// } +////} + +//@Filename: /node_modules/@emotion/css/index.d.ts +////export declare const css: any; +////const css2: any; +////export { css2 }; + +//@Filename: /node_modules/@emotion/css/package.json +////{ +//// "name": "@emotion/css", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/@emotion/core/index.d.ts +////import { css2 } from "@emotion/css"; +////export { css } from "@emotion/css"; +////export { css2 }; + +//@Filename: /node_modules/@emotion/core/package.json +////{ +//// "name": "@emotion/core", +//// "types": "./index.d.ts" +////} + +//@Filename: /src/index.ts +////cs/**/ + +verify.completions({ + marker: test.marker(""), + includes: [ + completion.undefinedVarEntry, + { + name: "css", + source: "/node_modules/@emotion/core/index", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "css2", + source: "/node_modules/@emotion/core/index", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ...completion.statementKeywordsWithTypes + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport2.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport2.ts new file mode 100644 index 0000000000000..eb946ce17b42c --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport2.ts @@ -0,0 +1,58 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "b_": "*", +//// "_c": "*" +//// } +////} + +//@Filename: /node_modules/a/index.d.ts +////export const foo = 0; + +//@Filename: /node_modules/a/package.json +////{ +//// "name": "a", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/b_/index.d.ts +////export { foo } from "a"; + +//@Filename: /node_modules/b_/package.json +////{ +//// "name": "b_", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/_c/index.d.ts +////export { foo } from "b_"; + +//@Filename: /node_modules/_c/package.json +////{ +//// "name": "_c", +//// "types": "./index.d.ts" +////} + +//@Filename: /src/index.ts +////fo/**/ + +verify.completions({ + marker: test.marker(""), + includes: [ + completion.undefinedVarEntry, + { + name: "foo", + source: "/node_modules/b_/index", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ...completion.statementKeywordsWithTypes + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport3.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport3.ts new file mode 100644 index 0000000000000..8533461e0b805 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport3.ts @@ -0,0 +1,48 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "b": "*", +//// } +////} + +//@Filename: /node_modules/a/index.d.ts +////export const foo = 0; + +//@Filename: /node_modules/a/package.json +////{ +//// "name": "a", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/b/index.d.ts +////export * from "a"; + +//@Filename: /node_modules/b/package.json +////{ +//// "name": "b", +//// "types": "./index.d.ts" +////} + +//@Filename: /src/index.ts +////fo/**/ + +verify.completions({ + marker: test.marker(""), + includes: [ + completion.undefinedVarEntry, + { + name: "foo", + source: "/node_modules/b/index", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ...completion.statementKeywordsWithTypes + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport4.ts b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport4.ts new file mode 100644 index 0000000000000..83ac6526b2586 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_filteredByPackageJson_reexport4.ts @@ -0,0 +1,57 @@ +/// + +//@noEmit: true + +//@Filename: /package.json +////{ +//// "dependencies": { +//// "c": "*", +//// } +////} + +//@Filename: /node_modules/a/index.d.ts +////export const foo = 0; + +//@Filename: /node_modules/a/package.json +////{ +//// "name": "a", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/b/index.d.ts +////export * from "a"; + +//@Filename: /node_modules/b/package.json +////{ +//// "name": "b", +//// "types": "./index.d.ts" +////} + +//@Filename: /node_modules/c/index.d.ts +////export * from "a"; + +//@Filename: /node_modules/c/package.json +////{ +//// "name": "c", +//// "types": "./index.d.ts" +////} + +//@Filename: /src/index.ts +////fo/**/ + +verify.completions({ + marker: test.marker(""), + includes: [ + completion.undefinedVarEntry, + { + name: "foo", + source: "/node_modules/c/index", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ...completion.statementKeywordsWithTypes + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); diff --git a/tests/cases/fourslash/completionsImport_ofAlias.ts b/tests/cases/fourslash/completionsImport_ofAlias.ts index 9a9cb4a2b18c1..1319391eb0bf1 100644 --- a/tests/cases/fourslash/completionsImport_ofAlias.ts +++ b/tests/cases/fourslash/completionsImport_ofAlias.ts @@ -16,6 +16,9 @@ // @Filename: /a_reexport_2.ts ////export * from "./a"; +// @Filename: /a_reexport_3.ts +////export { foo } from "./a_reexport"; + // @Filename: /b.ts ////fo/**/ @@ -24,13 +27,13 @@ verify.completions({ includes: [ completion.undefinedVarEntry, { - name: "foo", - source: "/a", - sourceDisplay: "./a", - text: "(alias) const foo: 0\nexport foo", - kind: "alias", - hasAction: true, - sortText: completion.SortText.AutoImportSuggestions + name: "foo", + source: "/a", + sourceDisplay: "./a", + text: "(alias) const foo: 0\nexport foo", + kind: "alias", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions }, ...completion.statementKeywordsWithTypes, ], diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules8.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules8.ts index f048f0d30d253..acfddd587f7ae 100644 --- a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules8.ts +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules8.ts @@ -3,7 +3,7 @@ //// [|f1/*0*/('');|] // @Filename: package.json -//// { "dependencies": { "package-name": "latest" } } +//// { "dependencies": { "@scope/package-name": "latest" } } // @Filename: node_modules/@scope/package-name/bin/lib/index.d.ts //// export function f1(text: string): string;