Skip to content

Commit 09caaf6

Browse files
authored
Add autoImportSpecifierExcludeRegexes preference (#59543)
1 parent 1bb1d2a commit 09caaf6

File tree

8 files changed

+183
-14
lines changed

8 files changed

+183
-14
lines changed

src/compiler/moduleSpecifiers.ts

+62-10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
mapDefined,
8484
MapLike,
8585
matchPatternOrExact,
86+
memoizeOne,
8687
min,
8788
ModuleDeclaration,
8889
ModuleKind,
@@ -127,6 +128,34 @@ import {
127128
UserPreferences,
128129
} from "./_namespaces/ts.js";
129130

131+
const stringToRegex = memoizeOne((pattern: string) => {
132+
try {
133+
let slash = pattern.indexOf("/");
134+
if (slash !== 0) {
135+
// No leading slash, treat as a pattern
136+
return new RegExp(pattern);
137+
}
138+
const lastSlash = pattern.lastIndexOf("/");
139+
if (slash === lastSlash) {
140+
// Only one slash, treat as a pattern
141+
return new RegExp(pattern);
142+
}
143+
while ((slash = pattern.indexOf("/", slash + 1)) !== lastSlash) {
144+
if (pattern[slash - 1] !== "\\") {
145+
// Unescaped middle slash, treat as a pattern
146+
return new RegExp(pattern);
147+
}
148+
}
149+
// Only case-insensitive and unicode flags make sense
150+
const flags = pattern.substring(lastSlash + 1).replace(/[^iu]/g, "");
151+
pattern = pattern.substring(1, lastSlash);
152+
return new RegExp(pattern, flags);
153+
}
154+
catch {
155+
return undefined;
156+
}
157+
});
158+
130159
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
131160

132161
/** @internal */
@@ -144,18 +173,20 @@ export interface ModuleSpecifierPreferences {
144173
* @param syntaxImpliedNodeFormat Used when the import syntax implies ESM or CJS irrespective of the mode of the file.
145174
*/
146175
getAllowedEndingsInPreferredOrder(syntaxImpliedNodeFormat?: ResolutionMode): ModuleSpecifierEnding[];
176+
readonly excludeRegexes?: readonly string[];
147177
}
148178

149179
/** @internal */
150180
export function getModuleSpecifierPreferences(
151-
{ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences,
181+
{ importModuleSpecifierPreference, importModuleSpecifierEnding, autoImportSpecifierExcludeRegexes }: UserPreferences,
152182
host: Pick<ModuleSpecifierResolutionHost, "getDefaultResolutionModeForFile">,
153183
compilerOptions: CompilerOptions,
154184
importingSourceFile: Pick<SourceFile, "fileName" | "impliedNodeFormat">,
155185
oldImportSpecifier?: string,
156186
): ModuleSpecifierPreferences {
157187
const filePreferredEnding = getPreferredEnding();
158188
return {
189+
excludeRegexes: autoImportSpecifierExcludeRegexes,
159190
relativePreference: oldImportSpecifier !== undefined ? (isExternalModuleNameRelative(oldImportSpecifier) ?
160191
RelativePreference.Relative :
161192
RelativePreference.NonRelative) :
@@ -362,7 +393,13 @@ export function getModuleSpecifiersWithCacheInfo(
362393
): ModuleSpecifierResult {
363394
let computedWithoutCache = false;
364395
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
365-
if (ambient) return { kind: "ambient", moduleSpecifiers: [ambient], computedWithoutCache };
396+
if (ambient) {
397+
return {
398+
kind: "ambient",
399+
moduleSpecifiers: !(forAutoImport && isExcludedByRegex(ambient, userPreferences.autoImportSpecifierExcludeRegexes)) ? [ambient] : emptyArray,
400+
computedWithoutCache,
401+
};
402+
}
366403

367404
// eslint-disable-next-line prefer-const
368405
let [kind, specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(
@@ -459,11 +496,13 @@ function computeModuleSpecifiers(
459496
const specifier = modulePath.isInNodeModules
460497
? tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode)
461498
: undefined;
462-
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
463-
if (specifier && modulePath.isRedirect) {
464-
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
465-
// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
466-
return { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers!, computedWithoutCache: true };
499+
if (specifier && !(forAutoImport && isExcludedByRegex(specifier, preferences.excludeRegexes))) {
500+
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
501+
if (modulePath.isRedirect) {
502+
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
503+
// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
504+
return { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers, computedWithoutCache: true };
505+
}
467506
}
468507

469508
if (!specifier) {
@@ -476,7 +515,7 @@ function computeModuleSpecifiers(
476515
preferences,
477516
/*pathsOnly*/ modulePath.isRedirect,
478517
);
479-
if (!local) {
518+
if (!local || forAutoImport && isExcludedByRegex(local, preferences.excludeRegexes)) {
480519
continue;
481520
}
482521
if (modulePath.isRedirect) {
@@ -512,7 +551,11 @@ function computeModuleSpecifiers(
512551
return pathsSpecifiers?.length ? { kind: "paths", moduleSpecifiers: pathsSpecifiers, computedWithoutCache: true } :
513552
redirectPathsSpecifiers?.length ? { kind: "redirect", moduleSpecifiers: redirectPathsSpecifiers, computedWithoutCache: true } :
514553
nodeModulesSpecifiers?.length ? { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers, computedWithoutCache: true } :
515-
{ kind: "relative", moduleSpecifiers: Debug.checkDefined(relativeSpecifiers), computedWithoutCache: true };
554+
{ kind: "relative", moduleSpecifiers: relativeSpecifiers ?? emptyArray, computedWithoutCache: true };
555+
}
556+
557+
function isExcludedByRegex(moduleSpecifier: string, excludeRegexes: readonly string[] | undefined): boolean {
558+
return some(excludeRegexes, pattern => !!stringToRegex(pattern)?.test(moduleSpecifier));
516559
}
517560

518561
interface Info {
@@ -536,7 +579,7 @@ function getInfo(importingSourceFileName: string, host: ModuleSpecifierResolutio
536579

537580
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: ModuleSpecifierPreferences): string;
538581
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined;
539-
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { getAllowedEndingsInPreferredOrder: getAllowedEndingsInPrefererredOrder, relativePreference }: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined {
582+
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { getAllowedEndingsInPreferredOrder: getAllowedEndingsInPrefererredOrder, relativePreference, excludeRegexes }: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined {
540583
const { baseUrl, paths, rootDirs } = compilerOptions;
541584
if (pathsOnly && !paths) {
542585
return undefined;
@@ -568,6 +611,15 @@ function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOpt
568611
return relativePath;
569612
}
570613

614+
const relativeIsExcluded = isExcludedByRegex(relativePath, excludeRegexes);
615+
const nonRelativeIsExcluded = isExcludedByRegex(maybeNonRelative, excludeRegexes);
616+
if (!relativeIsExcluded && nonRelativeIsExcluded) {
617+
return relativePath;
618+
}
619+
if (relativeIsExcluded && !nonRelativeIsExcluded) {
620+
return maybeNonRelative;
621+
}
622+
571623
if (relativePreference === RelativePreference.NonRelative && !pathIsRelative(maybeNonRelative)) {
572624
return maybeNonRelative;
573625
}

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10260,6 +10260,7 @@ export interface UserPreferences {
1026010260
readonly interactiveInlayHints?: boolean;
1026110261
readonly allowRenameOfImportPath?: boolean;
1026210262
readonly autoImportFileExcludePatterns?: string[];
10263+
readonly autoImportSpecifierExcludeRegexes?: string[];
1026310264
readonly preferTypeOnlyAutoImports?: boolean;
1026410265
/**
1026510266
* Indicates whether imports should be organized in a case-insensitive manner.

src/services/completions.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ import {
8484
getEffectiveBaseTypeNode,
8585
getEffectiveModifierFlags,
8686
getEffectiveTypeAnnotationNode,
87-
getEmitModuleResolutionKind,
8887
getEmitScriptTarget,
8988
getEscapedTextOfIdentifierOrLiteral,
9089
getEscapedTextOfJsxAttributeName,
@@ -106,6 +105,7 @@ import {
106105
getPropertyNameForPropertyNameNode,
107106
getQuotePreference,
108107
getReplacementSpanForContextToken,
108+
getResolvePackageJsonExports,
109109
getRootDeclaration,
110110
getSourceFileOfModule,
111111
getSwitchedType,
@@ -301,7 +301,6 @@ import {
301301
ModuleDeclaration,
302302
moduleExportNameTextEscaped,
303303
ModuleReference,
304-
moduleResolutionSupportsPackageJsonExportsAndImports,
305304
NamedImportBindings,
306305
newCaseClauseTracker,
307306
Node,
@@ -629,12 +628,16 @@ function resolvingModuleSpecifiers<TReturn>(
629628
cb: (context: ModuleSpecifierResolutionContext) => TReturn,
630629
): TReturn {
631630
const start = timestamp();
632-
// Under `--moduleResolution nodenext`, we have to resolve module specifiers up front, because
631+
// Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because
633632
// package.json exports can mean we *can't* resolve a module specifier (that doesn't include a
634633
// relative path into node_modules), and we want to filter those completions out entirely.
635634
// Import statement completions always need specifier resolution because the module specifier is
636635
// part of their `insertText`, not the `codeActions` creating edits away from the cursor.
637-
const needsFullResolution = isForImportStatementCompletion || moduleResolutionSupportsPackageJsonExportsAndImports(getEmitModuleResolutionKind(program.getCompilerOptions()));
636+
// Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers
637+
// because completion items are being explcitly filtered out by module specifier.
638+
const needsFullResolution = isForImportStatementCompletion
639+
|| getResolvePackageJsonExports(program.getCompilerOptions())
640+
|| preferences.autoImportSpecifierExcludeRegexes?.length;
638641
let skippedAny = false;
639642
let ambientCount = 0;
640643
let resolvedCount = 0;

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

+1
Original file line numberDiff line numberDiff line change
@@ -8241,6 +8241,7 @@ declare namespace ts {
82418241
readonly interactiveInlayHints?: boolean;
82428242
readonly allowRenameOfImportPath?: boolean;
82438243
readonly autoImportFileExcludePatterns?: string[];
8244+
readonly autoImportSpecifierExcludeRegexes?: string[];
82448245
readonly preferTypeOnlyAutoImports?: boolean;
82458246
/**
82468247
* Indicates whether imports should be organized in a case-insensitive manner.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: preserve
4+
5+
// @Filename: /node_modules/lib/index.d.ts
6+
//// declare module "ambient" {
7+
//// export const x: number;
8+
//// }
9+
//// declare module "ambient/utils" {
10+
//// export const x: number;
11+
//// }
12+
13+
// @Filename: /index.ts
14+
//// x/**/
15+
16+
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"]);
17+
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["utils"] });
18+
// case sensitive, no match
19+
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["/UTILS/"] });
20+
// case insensitive flag given
21+
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/UTILS/i"] });
22+
// invalid due to unescaped slash, treated as pattern
23+
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["/ambient/utils/"] });
24+
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/ambient\\/utils/"] });
25+
// no trailing slash, treated as pattern, slash doesn't need to be escaped
26+
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/.*?$"]});
27+
// no leading slash, treated as pattern, slash doesn't need to be escaped
28+
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["^ambient/"] });
29+
verify.importFixModuleSpecifiers("", ["ambient/utils"], { autoImportSpecifierExcludeRegexes: ["ambient$"] });
30+
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["oops("] });
31+
32+
verify.completions({
33+
marker: "",
34+
includes: [{
35+
name: "x",
36+
source: "ambient",
37+
sourceDisplay: "ambient",
38+
hasAction: true,
39+
sortText: completion.SortText.AutoImportSuggestions
40+
}, {
41+
name: "x",
42+
source: "ambient/utils",
43+
sourceDisplay: "ambient/utils",
44+
hasAction: true,
45+
sortText: completion.SortText.AutoImportSuggestions
46+
}],
47+
preferences: {
48+
includeCompletionsForModuleExports: true,
49+
allowIncompleteCompletions: true
50+
}
51+
});
52+
53+
verify.completions({
54+
marker: "",
55+
excludes: ["ambient/utils"],
56+
preferences: {
57+
includeCompletionsForModuleExports: true,
58+
allowIncompleteCompletions: true,
59+
autoImportSpecifierExcludeRegexes: ["utils"]
60+
},
61+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /tsconfig.json
4+
//// {
5+
//// "compilerOptions": {
6+
//// "module": "preserve",
7+
//// "paths": {
8+
//// "@app/*": ["./src/*"]
9+
//// }
10+
//// }
11+
//// }
12+
13+
// @Filename: /src/utils.ts
14+
//// export function add(a: number, b: number) {}
15+
16+
// @Filename: /src/index.ts
17+
//// add/**/
18+
19+
verify.importFixModuleSpecifiers("", ["./utils"]);
20+
verify.importFixModuleSpecifiers("", ["@app/utils"], { autoImportSpecifierExcludeRegexes: ["^\\./"] });
21+
22+
verify.importFixModuleSpecifiers("", ["@app/utils"], { importModuleSpecifierPreference: "non-relative" });
23+
verify.importFixModuleSpecifiers("", ["./utils"], { importModuleSpecifierPreference: "non-relative", autoImportSpecifierExcludeRegexes: ["^@app/"] });
24+
25+
verify.importFixModuleSpecifiers("", [], { autoImportSpecifierExcludeRegexes: ["utils"] });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: preserve
4+
5+
// @Filename: /node_modules/pkg/package.json
6+
//// {
7+
//// "name": "pkg",
8+
//// "version": "1.0.0",
9+
//// "exports": {
10+
//// ".": "./index.js",
11+
//// "./utils": "./utils.js"
12+
//// }
13+
//// }
14+
15+
// @Filename: /node_modules/pkg/utils.d.ts
16+
//// export function add(a: number, b: number) {}
17+
18+
// @Filename: /node_modules/pkg/index.d.ts
19+
//// export * from "./utils";
20+
21+
// @Filename: /src/index.ts
22+
//// add/**/
23+
24+
verify.importFixModuleSpecifiers("", ["pkg", "pkg/utils"]);
25+
verify.importFixModuleSpecifiers("", ["pkg/utils"], { autoImportSpecifierExcludeRegexes: ["^pkg$"] });

tests/cases/fourslash/fourslash.ts

+1
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ declare namespace FourSlashInterface {
686686
readonly providePrefixAndSuffixTextForRename?: boolean;
687687
readonly allowRenameOfImportPath?: boolean;
688688
readonly autoImportFileExcludePatterns?: readonly string[];
689+
readonly autoImportSpecifierExcludeRegexes?: readonly string[];
689690
readonly preferTypeOnlyAutoImports?: boolean;
690691
readonly organizeImportsIgnoreCase?: "auto" | boolean;
691692
readonly organizeImportsCollation?: "unicode" | "ordinal";

0 commit comments

Comments
 (0)