From babd1a5f6ac6ff619fbd24c9ab8ae552a4ed9ed6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 13 Jun 2022 17:26:11 -0700 Subject: [PATCH 1/4] Basic functionality --- src/compiler/types.ts | 1 + src/services/codefixes/importFixes.ts | 4 ++-- src/services/completions.ts | 4 ++-- src/services/exportInfoMap.ts | 24 ++++++++++++++++--- .../autoImportFileExcludePatterns1.ts | 22 +++++++++++++++++ tests/cases/fourslash/fourslash.ts | 1 + 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 tests/cases/fourslash/autoImportFileExcludePatterns1.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a6121cddb6096..b512691685748 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8989,6 +8989,7 @@ namespace ts { readonly includeInlayPropertyDeclarationTypeHints?: boolean; readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ 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 2f6b940f7cf7d..f665610ecb11f 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..f27d338fa1a83 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -336,10 +336,28 @@ 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 => { + const pattern = getPatternFromSpec(spec, "/", "exclude"); + return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames) : undefined; + }); + + forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => { + if (excludePatterns) { + if (file && excludePatterns.some(p => p.test(file.fileName))) { + return; + } + if (!file && module.declarations?.every(d => excludePatterns.some(p => p.test(d.getSourceFile().fileName)))) { + return; + } + } + cb(module, file, program, /*isFromPackageJson*/ false); + }); + const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); if (autoImportProvider) { const start = timestamp(); @@ -361,7 +379,7 @@ namespace ts { } } - 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 +400,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/cases/fourslash/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/autoImportFileExcludePatterns1.ts new file mode 100644 index 0000000000000..964c9bac3a373 --- /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/fourslash.ts b/tests/cases/fourslash/fourslash.ts index ea690be401430..7d739c1e7d30b 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -660,6 +660,7 @@ declare namespace FourSlashInterface { readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none"; + readonly autoImportFileExcludePatterns?: readonly string[]; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; From ae40b6d600fac3fe3abb33de43a3f5a46a5a6332 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 16 Jun 2022 09:54:41 -0700 Subject: [PATCH 2/4] Add tests --- src/services/exportInfoMap.ts | 26 +++++-------- .../autoImportFileExcludePatterns2.ts | 39 +++++++++++++++++++ .../server/autoImportFileExcludePatterns1.ts | 31 +++++++++++++++ 3 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 tests/cases/fourslash/autoImportFileExcludePatterns2.ts create mode 100644 tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index f27d338fa1a83..964079a8604fe 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -342,38 +342,30 @@ namespace ts { ) { const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); const excludePatterns = preferences.autoImportFileExcludePatterns && mapDefined(preferences.autoImportFileExcludePatterns, spec => { - const pattern = getPatternFromSpec(spec, "/", "exclude"); + // 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(), (module, file) => { - if (excludePatterns) { - if (file && excludePatterns.some(p => p.test(file.fileName))) { - return; - } - if (!file && module.declarations?.every(d => excludePatterns.some(p => p.test(d.getSourceFile().fileName)))) { - return; - } - } - cb(module, file, program, /*isFromPackageJson*/ false); - }); - + 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); } } diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns2.ts b/tests/cases/fourslash/autoImportFileExcludePatterns2.ts new file mode 100644 index 0000000000000..70088eba319ae --- /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/server/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts new file mode 100644 index 0000000000000..be4bcd5b3346d --- /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 }); From 390ac3d811deda741c56525af8fc22b2420be200 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 16 Jun 2022 10:17:50 -0700 Subject: [PATCH 3/4] Add test for ambient modules --- .../autoImportFileExcludePatterns1.ts | 2 +- .../autoImportFileExcludePatterns2.ts | 4 +- .../autoImportFileExcludePatterns3.ts | 52 +++++++++++++++++++ .../server/autoImportFileExcludePatterns1.ts | 2 +- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/cases/fourslash/autoImportFileExcludePatterns3.ts diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/autoImportFileExcludePatterns1.ts index 964c9bac3a373..d4e7cb65cde88 100644 --- a/tests/cases/fourslash/autoImportFileExcludePatterns1.ts +++ b/tests/cases/fourslash/autoImportFileExcludePatterns1.ts @@ -8,7 +8,7 @@ // @Filename: /project/index.ts //// S3/**/ -const autoImportFileExcludePatterns = ["**/node_modules/aws-sdk"]; +const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"]; verify.completions({ marker: "", diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns2.ts b/tests/cases/fourslash/autoImportFileExcludePatterns2.ts index 70088eba319ae..8c5296189a2cd 100644 --- a/tests/cases/fourslash/autoImportFileExcludePatterns2.ts +++ b/tests/cases/fourslash/autoImportFileExcludePatterns2.ts @@ -30,10 +30,10 @@ verify.completions({ preferences: { allowIncompleteCompletions: true, includeCompletionsForModuleExports: true, - autoImportFileExcludePatterns: ["**/index.*"], + autoImportFileExcludePatterns: ["/**/index.*"], }, }); verify.importFixModuleSpecifiers("", ["./lib/main", "./lib/components/button/Button"], - { autoImportFileExcludePatterns: ["**/index.*"] }); + { 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/server/autoImportFileExcludePatterns1.ts b/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts index be4bcd5b3346d..2b2a7c9b9632f 100644 --- a/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts +++ b/tests/cases/fourslash/server/autoImportFileExcludePatterns1.ts @@ -17,7 +17,7 @@ // @Filename: /project/index.ts //// S3/**/ -const autoImportFileExcludePatterns = ["**/node_modules/aws-sdk"]; +const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"]; verify.completions({ marker: "", From 04063b15de351945637e003ac39100a382122463 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 16 Jun 2022 10:28:30 -0700 Subject: [PATCH 4/4] Add to protocol --- src/server/protocol.ts | 1 + tests/baselines/reference/api/tsserverlibrary.d.ts | 2 ++ tests/baselines/reference/api/typescript.d.ts | 1 + 3 files changed, 4 insertions(+) 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/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 9401f57e76edf..00f0000071278 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4137,6 +4137,7 @@ declare namespace ts { readonly includeInlayPropertyDeclarationTypeHints?: boolean; readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { @@ -9758,6 +9759,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 aaea770c3efcb..b00db38b82316 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4137,6 +4137,7 @@ declare namespace ts { readonly includeInlayPropertyDeclarationTypeHints?: boolean; readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; + readonly autoImportFileExcludePatterns?: string[]; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt {