Skip to content

Commit 7e7c539

Browse files
authored
Add TS Server option to exclude files from auto-imports (microsoft#49578)
* Basic functionality * Add tests * Add test for ambient modules * Add to protocol
1 parent 1213c35 commit 7e7c539

12 files changed

+171
-11
lines changed

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8990,6 +8990,7 @@ namespace ts {
89908990
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
89918991
readonly includeInlayEnumMemberValueHints?: boolean;
89928992
readonly allowRenameOfImportPath?: boolean;
8993+
readonly autoImportFileExcludePatterns?: string[];
89938994
}
89948995

89958996
/** Represents a bigint literal value without requiring bigint support */

src/server/protocol.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3469,6 +3469,7 @@ namespace ts.server.protocol {
34693469
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
34703470
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
34713471
readonly includeInlayEnumMemberValueHints?: boolean;
3472+
readonly autoImportFileExcludePatterns?: string[];
34723473
}
34733474

34743475
export interface CompilerOptions {

src/services/codefixes/importFixes.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ namespace ts.codefix {
413413
return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host);
414414
});
415415

416-
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
416+
forEachExternalModuleToImportFrom(program, host, preferences, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
417417
const checker = program.getTypeChecker();
418418
// Don't import from a re-export when looking "up" like to `./index` or `../index`.
419419
if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) {
@@ -979,7 +979,7 @@ namespace ts.codefix {
979979
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, moduleFileName: toFile?.fileName, exportKind, targetFlags: skipAlias(exportedSymbol, checker).flags, isFromPackageJson });
980980
}
981981
}
982-
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => {
982+
forEachExternalModuleToImportFrom(program, host, preferences, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => {
983983
const checker = program.getTypeChecker();
984984
cancellationToken.throwIfCancellationRequested();
985985

src/services/completions.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ namespace ts.Completions {
366366
if (!previousResponse) return undefined;
367367

368368
const lowerCaseTokenText = location.text.toLowerCase();
369-
const exportMap = getExportInfoMap(file, host, program, cancellationToken);
369+
const exportMap = getExportInfoMap(file, host, program, preferences, cancellationToken);
370370
const newEntries = resolvingModuleSpecifiers(
371371
"continuePreviousIncompleteResponse",
372372
host,
@@ -2725,7 +2725,7 @@ namespace ts.Completions {
27252725
"";
27262726

27272727
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
2728-
const exportInfo = getExportInfoMap(sourceFile, host, program, cancellationToken);
2728+
const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken);
27292729
const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.();
27302730
const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host);
27312731
resolvingModuleSpecifiers(

src/services/exportInfoMap.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -336,32 +336,42 @@ namespace ts {
336336
export function forEachExternalModuleToImportFrom(
337337
program: Program,
338338
host: LanguageServiceHost,
339+
preferences: UserPreferences,
339340
useAutoImportProvider: boolean,
340341
cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void,
341342
) {
342-
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
343+
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
344+
const excludePatterns = preferences.autoImportFileExcludePatterns && mapDefined(preferences.autoImportFileExcludePatterns, spec => {
345+
// The client is expected to send rooted path specs since we don't know
346+
// what directory a relative path is relative to.
347+
const pattern = getPatternFromSpec(spec, "", "exclude");
348+
return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames) : undefined;
349+
});
350+
351+
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), excludePatterns, (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
343352
const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.();
344353
if (autoImportProvider) {
345354
const start = timestamp();
346-
forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
355+
forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
347356
host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`);
348357
}
349358
}
350359

351-
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
360+
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], excludePatterns: readonly RegExp[] | undefined, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
361+
const isExcluded = (fileName: string) => excludePatterns?.some(p => p.test(fileName));
352362
for (const ambient of checker.getAmbientModules()) {
353-
if (!stringContains(ambient.name, "*")) {
363+
if (!stringContains(ambient.name, "*") && !(excludePatterns && ambient.declarations?.every(d => isExcluded(d.getSourceFile().fileName)))) {
354364
cb(ambient, /*sourceFile*/ undefined);
355365
}
356366
}
357367
for (const sourceFile of allSourceFiles) {
358-
if (isExternalOrCommonJsModule(sourceFile)) {
368+
if (isExternalOrCommonJsModule(sourceFile) && !isExcluded(sourceFile.fileName)) {
359369
cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile);
360370
}
361371
}
362372
}
363373

364-
export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, cancellationToken: CancellationToken | undefined): ExportInfoMap {
374+
export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap {
365375
const start = timestamp();
366376
// Pulling the AutoImportProvider project will trigger its updateGraph if pending,
367377
// which will invalidate the export map cache if things change, so pull it before
@@ -382,7 +392,7 @@ namespace ts {
382392
const compilerOptions = program.getCompilerOptions();
383393
let moduleCount = 0;
384394
try {
385-
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
395+
forEachExternalModuleToImportFrom(program, host, preferences, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
386396
if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested();
387397
const seenExports = new Map<__String, true>();
388398
const checker = program.getTypeChecker();

tests/baselines/reference/api/tsserverlibrary.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4138,6 +4138,7 @@ declare namespace ts {
41384138
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
41394139
readonly includeInlayEnumMemberValueHints?: boolean;
41404140
readonly allowRenameOfImportPath?: boolean;
4141+
readonly autoImportFileExcludePatterns?: string[];
41414142
}
41424143
/** Represents a bigint literal value without requiring bigint support */
41434144
export interface PseudoBigInt {
@@ -9769,6 +9770,7 @@ declare namespace ts.server.protocol {
97699770
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
97709771
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
97719772
readonly includeInlayEnumMemberValueHints?: boolean;
9773+
readonly autoImportFileExcludePatterns?: string[];
97729774
}
97739775
interface CompilerOptions {
97749776
allowJs?: boolean;

tests/baselines/reference/api/typescript.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4138,6 +4138,7 @@ declare namespace ts {
41384138
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
41394139
readonly includeInlayEnumMemberValueHints?: boolean;
41404140
readonly allowRenameOfImportPath?: boolean;
4141+
readonly autoImportFileExcludePatterns?: string[];
41414142
}
41424143
/** Represents a bigint literal value without requiring bigint support */
41434144
export interface PseudoBigInt {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @module: commonjs
4+
5+
// @Filename: /project/node_modules/aws-sdk/clients/s3.d.ts
6+
//// export declare class S3 {}
7+
8+
// @Filename: /project/index.ts
9+
//// S3/**/
10+
11+
const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"];
12+
13+
verify.completions({
14+
marker: "",
15+
excludes: "S3",
16+
preferences: {
17+
includeCompletionsForModuleExports: true,
18+
autoImportFileExcludePatterns,
19+
}
20+
});
21+
22+
verify.importFixAtPosition([], /*errorCode*/ undefined, { autoImportFileExcludePatterns });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /lib/components/button/Button.ts
4+
//// export function Button() {}
5+
6+
// @Filename: /lib/components/button/index.ts
7+
//// export * from "./Button";
8+
9+
// @Filename: /lib/components/index.ts
10+
//// export * from "./button";
11+
12+
// @Filename: /lib/main.ts
13+
//// export { Button } from "./components";
14+
15+
// @Filename: /lib/index.ts
16+
//// export * from "./main";
17+
18+
// @Filename: /i-hate-index-files.ts
19+
//// Button/**/
20+
21+
verify.completions({
22+
marker: "",
23+
exact: completion.globalsPlus([{
24+
name: "Button",
25+
source: "./lib/main",
26+
sourceDisplay: "./lib/main",
27+
hasAction: true,
28+
sortText: completion.SortText.AutoImportSuggestions,
29+
}]),
30+
preferences: {
31+
allowIncompleteCompletions: true,
32+
includeCompletionsForModuleExports: true,
33+
autoImportFileExcludePatterns: ["/**/index.*"],
34+
},
35+
});
36+
37+
verify.importFixModuleSpecifiers("",
38+
["./lib/main", "./lib/components/button/Button"],
39+
{ autoImportFileExcludePatterns: ["/**/index.*"] });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: commonjs
4+
5+
// @Filename: /ambient1.d.ts
6+
//// declare module "foo" {
7+
//// export const x = 1;
8+
//// }
9+
10+
// @Filename: /ambient2.d.ts
11+
//// declare module "foo" {
12+
//// export const y = 2;
13+
//// }
14+
15+
// @Filename: /index.ts
16+
//// /**/
17+
18+
verify.completions({
19+
marker: "",
20+
exact: completion.globalsPlus([{
21+
// We don't look at what file each individual export came from; we
22+
// only include or exclude modules wholesale, so excluding part of
23+
// an ambient module or a module augmentation isn't supported.
24+
name: "x",
25+
source: "foo",
26+
sourceDisplay: "foo",
27+
hasAction: true,
28+
sortText: completion.SortText.AutoImportSuggestions,
29+
}, {
30+
name: "y",
31+
source: "foo",
32+
sourceDisplay: "foo",
33+
hasAction: true,
34+
sortText: completion.SortText.AutoImportSuggestions,
35+
}]),
36+
preferences: {
37+
allowIncompleteCompletions: true,
38+
includeCompletionsForModuleExports: true,
39+
autoImportFileExcludePatterns: ["/**/ambient1.d.ts"],
40+
}
41+
});
42+
43+
// Here, *every* file that declared "foo" is excluded.
44+
verify.completions({
45+
marker: "",
46+
exact: completion.globals,
47+
preferences: {
48+
allowIncompleteCompletions: true,
49+
includeCompletionsForModuleExports: true,
50+
autoImportFileExcludePatterns: ["/**/ambient*"],
51+
}
52+
});

tests/cases/fourslash/fourslash.ts

+1
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ declare namespace FourSlashInterface {
662662
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
663663
readonly providePrefixAndSuffixTextForRename?: boolean;
664664
readonly allowRenameOfImportPath?: boolean;
665+
readonly autoImportFileExcludePatterns?: readonly string[];
665666
}
666667
interface InlayHintsOptions extends UserPreferences {
667668
readonly includeInlayParameterNameHints?: "none" | "literals" | "all";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// <reference path="../fourslash.ts"/>
2+
3+
// @module: commonjs
4+
5+
// @Filename: /project/node_modules/aws-sdk/package.json
6+
//// { "name": "aws-sdk", "version": "2.0.0", "main": "index.js" }
7+
8+
// @Filename: /project/node_modules/aws-sdk/index.d.ts
9+
//// export * from "./clients/s3";
10+
11+
// @Filename: /project/node_modules/aws-sdk/clients/s3.d.ts
12+
//// export declare class S3 {}
13+
14+
// @Filename: /project/package.json
15+
//// { "dependencies": "aws-sdk" }
16+
17+
// @Filename: /project/index.ts
18+
//// S3/**/
19+
20+
const autoImportFileExcludePatterns = ["/**/node_modules/aws-sdk"];
21+
22+
verify.completions({
23+
marker: "",
24+
excludes: "S3",
25+
preferences: {
26+
includeCompletionsForModuleExports: true,
27+
autoImportFileExcludePatterns,
28+
}
29+
});
30+
31+
verify.importFixAtPosition([], /*errorCode*/ undefined, { autoImportFileExcludePatterns });

0 commit comments

Comments
 (0)