diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f505da17dd1a3..caaad5fb9ece0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8990,6 +8990,7 @@ namespace ts { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 623699de435d6..e78d2262b5bca 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3469,6 +3469,7 @@ namespace ts.server.protocol { readonly includeInlayPropertyDeclarationTypeHints?: boolean; readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } export interface CompilerOptions { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index d1ea8927fc18a..1507689f6103c 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -413,7 +413,7 @@ namespace ts.codefix { return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); }); - forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, preferences, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); // Don't import from a re-export when looking "up" like to `./index` or `../index`. if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) { @@ -979,7 +979,7 @@ namespace ts.codefix { originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, moduleFileName: toFile?.fileName, exportKind, targetFlags: skipAlias(exportedSymbol, checker).flags, isFromPackageJson }); } } - forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, preferences, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); cancellationToken.throwIfCancellationRequested(); diff --git a/src/services/completions.ts b/src/services/completions.ts index 9e5ae92726972..5d20343fff45e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -366,7 +366,7 @@ namespace ts.Completions { if (!previousResponse) return undefined; const lowerCaseTokenText = location.text.toLowerCase(); - const exportMap = getExportInfoMap(file, host, program, cancellationToken); + const exportMap = getExportInfoMap(file, host, program, preferences, cancellationToken); const newEntries = resolvingModuleSpecifiers( "continuePreviousIncompleteResponse", host, @@ -2725,7 +2725,7 @@ namespace ts.Completions { ""; const moduleSpecifierCache = host.getModuleSpecifierCache?.(); - const exportInfo = getExportInfoMap(sourceFile, host, program, cancellationToken); + const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken); const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host); resolvingModuleSpecifiers( diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index e8b52d48122ab..964079a8604fe 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -336,32 +336,42 @@ namespace ts { export function forEachExternalModuleToImportFrom( program: Program, host: LanguageServiceHost, + preferences: UserPreferences, useAutoImportProvider: boolean, cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void, ) { - forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const excludePatterns = preferences.autoImportFileExcludePatterns && mapDefined(preferences.autoImportFileExcludePatterns, spec => { + // The client is expected to send rooted path specs since we don't know + // what directory a relative path is relative to. + const pattern = getPatternFromSpec(spec, "", "exclude"); + return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames) : undefined; + }); + + forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), excludePatterns, (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); if (autoImportProvider) { const start = timestamp(); - forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); + forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`); } } - function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { + function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], excludePatterns: readonly RegExp[] | undefined, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { + const isExcluded = (fileName: string) => excludePatterns?.some(p => p.test(fileName)); for (const ambient of checker.getAmbientModules()) { - if (!stringContains(ambient.name, "*")) { + if (!stringContains(ambient.name, "*") && !(excludePatterns && ambient.declarations?.every(d => isExcluded(d.getSourceFile().fileName)))) { cb(ambient, /*sourceFile*/ undefined); } } for (const sourceFile of allSourceFiles) { - if (isExternalOrCommonJsModule(sourceFile)) { + if (isExternalOrCommonJsModule(sourceFile) && !isExcluded(sourceFile.fileName)) { cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile); } } } - export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, cancellationToken: CancellationToken | undefined): ExportInfoMap { + export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { const start = timestamp(); // Pulling the AutoImportProvider project will trigger its updateGraph if pending, // which will invalidate the export map cache if things change, so pull it before @@ -382,7 +392,7 @@ namespace ts { const compilerOptions = program.getCompilerOptions(); let moduleCount = 0; try { - forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, preferences, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested(); const seenExports = new Map<__String, true>(); const checker = program.getTypeChecker(); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 264708cf29fb9..fb15ec4f0113b 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4138,6 +4138,7 @@ declare namespace ts { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { @@ -9769,6 +9770,7 @@ declare namespace ts.server.protocol { readonly includeInlayPropertyDeclarationTypeHints?: boolean; readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } interface CompilerOptions { allowJs?: boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c8cf181dff4ac..ed87f7ef8913b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4138,6 +4138,7 @@ declare namespace ts { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/autoImportFileExcludePatterns1.ts new file mode 100644 index 0000000000000..d4e7cb65cde88 --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns1.ts @@ -0,0 +1,22 @@ +/// + +// @module: commonjs + +// @Filename: /project/node_modules/aws-sdk/clients/s3.d.ts +//// export declare class S3 {} + +// @Filename: /project/index.ts +//// S3/**/ + +const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"]; + +verify.completions({ + marker: "", + excludes: "S3", + preferences: { + includeCompletionsForModuleExports: true, + autoImportFileExcludePatterns, + } +}); + +verify.importFixAtPosition([], /*errorCode*/ undefined, { autoImportFileExcludePatterns }); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns2.ts b/tests/cases/fourslash/autoImportFileExcludePatterns2.ts new file mode 100644 index 0000000000000..8c5296189a2cd --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns2.ts @@ -0,0 +1,39 @@ +/// + +// @Filename: /lib/components/button/Button.ts +//// export function Button() {} + +// @Filename: /lib/components/button/index.ts +//// export * from "./Button"; + +// @Filename: /lib/components/index.ts +//// export * from "./button"; + +// @Filename: /lib/main.ts +//// export { Button } from "./components"; + +// @Filename: /lib/index.ts +//// export * from "./main"; + +// @Filename: /i-hate-index-files.ts +//// Button/**/ + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "Button", + source: "./lib/main", + sourceDisplay: "./lib/main", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }]), + preferences: { + allowIncompleteCompletions: true, + includeCompletionsForModuleExports: true, + autoImportFileExcludePatterns: ["/**/index.*"], + }, +}); + +verify.importFixModuleSpecifiers("", + ["./lib/main", "./lib/components/button/Button"], + { autoImportFileExcludePatterns: ["/**/index.*"] }); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns3.ts b/tests/cases/fourslash/autoImportFileExcludePatterns3.ts new file mode 100644 index 0000000000000..8f8ff5d4a1ed7 --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns3.ts @@ -0,0 +1,52 @@ +/// + +// @module: commonjs + +// @Filename: /ambient1.d.ts +//// declare module "foo" { +//// export const x = 1; +//// } + +// @Filename: /ambient2.d.ts +//// declare module "foo" { +//// export const y = 2; +//// } + +// @Filename: /index.ts +//// /**/ + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + // We don't look at what file each individual export came from; we + // only include or exclude modules wholesale, so excluding part of + // an ambient module or a module augmentation isn't supported. + name: "x", + source: "foo", + sourceDisplay: "foo", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }, { + name: "y", + source: "foo", + sourceDisplay: "foo", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }]), + preferences: { + allowIncompleteCompletions: true, + includeCompletionsForModuleExports: true, + autoImportFileExcludePatterns: ["/**/ambient1.d.ts"], + } +}); + +// Here, *every* file that declared "foo" is excluded. +verify.completions({ + marker: "", + exact: completion.globals, + preferences: { + allowIncompleteCompletions: true, + includeCompletionsForModuleExports: true, + autoImportFileExcludePatterns: ["/**/ambient*"], + } +}); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 7f66f77644cb9..a6c1804affb4a 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -662,6 +662,7 @@ declare namespace FourSlashInterface { readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none"; readonly providePrefixAndSuffixTextForRename?: boolean; readonly allowRenameOfImportPath?: boolean; + readonly autoImportFileExcludePatterns?: readonly string[]; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; diff --git a/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts new file mode 100644 index 0000000000000..2b2a7c9b9632f --- /dev/null +++ b/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts @@ -0,0 +1,31 @@ +/// + +// @module: commonjs + +// @Filename: /project/node_modules/aws-sdk/package.json +//// { "name": "aws-sdk", "version": "2.0.0", "main": "index.js" } + +// @Filename: /project/node_modules/aws-sdk/index.d.ts +//// export * from "./clients/s3"; + +// @Filename: /project/node_modules/aws-sdk/clients/s3.d.ts +//// export declare class S3 {} + +// @Filename: /project/package.json +//// { "dependencies": "aws-sdk" } + +// @Filename: /project/index.ts +//// S3/**/ + +const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"]; + +verify.completions({ + marker: "", + excludes: "S3", + preferences: { + includeCompletionsForModuleExports: true, + autoImportFileExcludePatterns, + } +}); + +verify.importFixAtPosition([], /*errorCode*/ undefined, { autoImportFileExcludePatterns });