Add `autoImportSpecifierExcludeRegexes` preference (#59543)

This commit is contained in:
Andrew Branch 2024-08-09 11:12:18 -07:00 коммит произвёл GitHub
Родитель 1bb1d2a610
Коммит 09caaf60aa
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 183 добавлений и 14 удалений

Просмотреть файл

@ -83,6 +83,7 @@ import {
mapDefined,
MapLike,
matchPatternOrExact,
memoizeOne,
min,
ModuleDeclaration,
ModuleKind,
@ -127,6 +128,34 @@ import {
UserPreferences,
} from "./_namespaces/ts.js";
const stringToRegex = memoizeOne((pattern: string) => {
try {
let slash = pattern.indexOf("/");
if (slash !== 0) {
// No leading slash, treat as a pattern
return new RegExp(pattern);
}
const lastSlash = pattern.lastIndexOf("/");
if (slash === lastSlash) {
// Only one slash, treat as a pattern
return new RegExp(pattern);
}
while ((slash = pattern.indexOf("/", slash + 1)) !== lastSlash) {
if (pattern[slash - 1] !== "\\") {
// Unescaped middle slash, treat as a pattern
return new RegExp(pattern);
}
}
// Only case-insensitive and unicode flags make sense
const flags = pattern.substring(lastSlash + 1).replace(/[^iu]/g, "");
pattern = pattern.substring(1, lastSlash);
return new RegExp(pattern, flags);
}
catch {
return undefined;
}
});
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
/** @internal */
@ -144,11 +173,12 @@ export interface ModuleSpecifierPreferences {
* @param syntaxImpliedNodeFormat Used when the import syntax implies ESM or CJS irrespective of the mode of the file.
*/
getAllowedEndingsInPreferredOrder(syntaxImpliedNodeFormat?: ResolutionMode): ModuleSpecifierEnding[];
readonly excludeRegexes?: readonly string[];
}
/** @internal */
export function getModuleSpecifierPreferences(
{ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences,
{ importModuleSpecifierPreference, importModuleSpecifierEnding, autoImportSpecifierExcludeRegexes }: UserPreferences,
host: Pick<ModuleSpecifierResolutionHost, "getDefaultResolutionModeForFile">,
compilerOptions: CompilerOptions,
importingSourceFile: Pick<SourceFile, "fileName" | "impliedNodeFormat">,
@ -156,6 +186,7 @@ export function getModuleSpecifierPreferences(
): ModuleSpecifierPreferences {
const filePreferredEnding = getPreferredEnding();
return {
excludeRegexes: autoImportSpecifierExcludeRegexes,
relativePreference: oldImportSpecifier !== undefined ? (isExternalModuleNameRelative(oldImportSpecifier) ?
RelativePreference.Relative :
RelativePreference.NonRelative) :
@ -362,7 +393,13 @@ export function getModuleSpecifiersWithCacheInfo(
): ModuleSpecifierResult {
let computedWithoutCache = false;
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
if (ambient) return { kind: "ambient", moduleSpecifiers: [ambient], computedWithoutCache };
if (ambient) {
return {
kind: "ambient",
moduleSpecifiers: !(forAutoImport && isExcludedByRegex(ambient, userPreferences.autoImportSpecifierExcludeRegexes)) ? [ambient] : emptyArray,
computedWithoutCache,
};
}
// eslint-disable-next-line prefer-const
let [kind, specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(
@ -459,11 +496,13 @@ function computeModuleSpecifiers(
const specifier = modulePath.isInNodeModules
? tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode)
: undefined;
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
if (specifier && modulePath.isRedirect) {
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
return { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers!, computedWithoutCache: true };
if (specifier && !(forAutoImport && isExcludedByRegex(specifier, preferences.excludeRegexes))) {
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
if (modulePath.isRedirect) {
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
return { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers, computedWithoutCache: true };
}
}
if (!specifier) {
@ -476,7 +515,7 @@ function computeModuleSpecifiers(
preferences,
/*pathsOnly*/ modulePath.isRedirect,
);
if (!local) {
if (!local || forAutoImport && isExcludedByRegex(local, preferences.excludeRegexes)) {
continue;
}
if (modulePath.isRedirect) {
@ -512,7 +551,11 @@ function computeModuleSpecifiers(
return pathsSpecifiers?.length ? { kind: "paths", moduleSpecifiers: pathsSpecifiers, computedWithoutCache: true } :
redirectPathsSpecifiers?.length ? { kind: "redirect", moduleSpecifiers: redirectPathsSpecifiers, computedWithoutCache: true } :
nodeModulesSpecifiers?.length ? { kind: "node_modules", moduleSpecifiers: nodeModulesSpecifiers, computedWithoutCache: true } :
{ kind: "relative", moduleSpecifiers: Debug.checkDefined(relativeSpecifiers), computedWithoutCache: true };
{ kind: "relative", moduleSpecifiers: relativeSpecifiers ?? emptyArray, computedWithoutCache: true };
}
function isExcludedByRegex(moduleSpecifier: string, excludeRegexes: readonly string[] | undefined): boolean {
return some(excludeRegexes, pattern => !!stringToRegex(pattern)?.test(moduleSpecifier));
}
interface Info {
@ -536,7 +579,7 @@ function getInfo(importingSourceFileName: string, host: ModuleSpecifierResolutio
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: ModuleSpecifierPreferences): string;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { getAllowedEndingsInPreferredOrder: getAllowedEndingsInPrefererredOrder, relativePreference }: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined {
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { getAllowedEndingsInPreferredOrder: getAllowedEndingsInPrefererredOrder, relativePreference, excludeRegexes }: ModuleSpecifierPreferences, pathsOnly?: boolean): string | undefined {
const { baseUrl, paths, rootDirs } = compilerOptions;
if (pathsOnly && !paths) {
return undefined;
@ -568,6 +611,15 @@ function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOpt
return relativePath;
}
const relativeIsExcluded = isExcludedByRegex(relativePath, excludeRegexes);
const nonRelativeIsExcluded = isExcludedByRegex(maybeNonRelative, excludeRegexes);
if (!relativeIsExcluded && nonRelativeIsExcluded) {
return relativePath;
}
if (relativeIsExcluded && !nonRelativeIsExcluded) {
return maybeNonRelative;
}
if (relativePreference === RelativePreference.NonRelative && !pathIsRelative(maybeNonRelative)) {
return maybeNonRelative;
}

Просмотреть файл

@ -10260,6 +10260,7 @@ export interface UserPreferences {
readonly interactiveInlayHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly autoImportSpecifierExcludeRegexes?: string[];
readonly preferTypeOnlyAutoImports?: boolean;
/**
* Indicates whether imports should be organized in a case-insensitive manner.

Просмотреть файл

@ -84,7 +84,6 @@ import {
getEffectiveBaseTypeNode,
getEffectiveModifierFlags,
getEffectiveTypeAnnotationNode,
getEmitModuleResolutionKind,
getEmitScriptTarget,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
@ -106,6 +105,7 @@ import {
getPropertyNameForPropertyNameNode,
getQuotePreference,
getReplacementSpanForContextToken,
getResolvePackageJsonExports,
getRootDeclaration,
getSourceFileOfModule,
getSwitchedType,
@ -301,7 +301,6 @@ import {
ModuleDeclaration,
moduleExportNameTextEscaped,
ModuleReference,
moduleResolutionSupportsPackageJsonExportsAndImports,
NamedImportBindings,
newCaseClauseTracker,
Node,
@ -629,12 +628,16 @@ function resolvingModuleSpecifiers<TReturn>(
cb: (context: ModuleSpecifierResolutionContext) => TReturn,
): TReturn {
const start = timestamp();
// Under `--moduleResolution nodenext`, we have to resolve module specifiers up front, because
// Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because
// package.json exports can mean we *can't* resolve a module specifier (that doesn't include a
// relative path into node_modules), and we want to filter those completions out entirely.
// Import statement completions always need specifier resolution because the module specifier is
// part of their `insertText`, not the `codeActions` creating edits away from the cursor.
const needsFullResolution = isForImportStatementCompletion || moduleResolutionSupportsPackageJsonExportsAndImports(getEmitModuleResolutionKind(program.getCompilerOptions()));
// Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers
// because completion items are being explcitly filtered out by module specifier.
const needsFullResolution = isForImportStatementCompletion
|| getResolvePackageJsonExports(program.getCompilerOptions())
|| preferences.autoImportSpecifierExcludeRegexes?.length;
let skippedAny = false;
let ambientCount = 0;
let resolvedCount = 0;

Просмотреть файл

@ -8241,6 +8241,7 @@ declare namespace ts {
readonly interactiveInlayHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly autoImportSpecifierExcludeRegexes?: string[];
readonly preferTypeOnlyAutoImports?: boolean;
/**
* Indicates whether imports should be organized in a case-insensitive manner.

Просмотреть файл

@ -0,0 +1,61 @@
/// <reference path="fourslash.ts" />
// @module: preserve
// @Filename: /node_modules/lib/index.d.ts
//// declare module "ambient" {
//// export const x: number;
//// }
//// declare module "ambient/utils" {
//// export const x: number;
//// }
// @Filename: /index.ts
//// x/**/
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"]);
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["utils"] });
// case sensitive, no match
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["/UTILS/"] });
// case insensitive flag given
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/UTILS/i"] });
// invalid due to unescaped slash, treated as pattern
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["/ambient/utils/"] });
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/ambient\\/utils/"] });
// no trailing slash, treated as pattern, slash doesn't need to be escaped
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["/.*?$"]});
// no leading slash, treated as pattern, slash doesn't need to be escaped
verify.importFixModuleSpecifiers("", ["ambient"], { autoImportSpecifierExcludeRegexes: ["^ambient/"] });
verify.importFixModuleSpecifiers("", ["ambient/utils"], { autoImportSpecifierExcludeRegexes: ["ambient$"] });
verify.importFixModuleSpecifiers("", ["ambient", "ambient/utils"], { autoImportSpecifierExcludeRegexes: ["oops("] });
verify.completions({
marker: "",
includes: [{
name: "x",
source: "ambient",
sourceDisplay: "ambient",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
}, {
name: "x",
source: "ambient/utils",
sourceDisplay: "ambient/utils",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
}],
preferences: {
includeCompletionsForModuleExports: true,
allowIncompleteCompletions: true
}
});
verify.completions({
marker: "",
excludes: ["ambient/utils"],
preferences: {
includeCompletionsForModuleExports: true,
allowIncompleteCompletions: true,
autoImportSpecifierExcludeRegexes: ["utils"]
},
})

Просмотреть файл

@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
// @Filename: /tsconfig.json
//// {
//// "compilerOptions": {
//// "module": "preserve",
//// "paths": {
//// "@app/*": ["./src/*"]
//// }
//// }
//// }
// @Filename: /src/utils.ts
//// export function add(a: number, b: number) {}
// @Filename: /src/index.ts
//// add/**/
verify.importFixModuleSpecifiers("", ["./utils"]);
verify.importFixModuleSpecifiers("", ["@app/utils"], { autoImportSpecifierExcludeRegexes: ["^\\./"] });
verify.importFixModuleSpecifiers("", ["@app/utils"], { importModuleSpecifierPreference: "non-relative" });
verify.importFixModuleSpecifiers("", ["./utils"], { importModuleSpecifierPreference: "non-relative", autoImportSpecifierExcludeRegexes: ["^@app/"] });
verify.importFixModuleSpecifiers("", [], { autoImportSpecifierExcludeRegexes: ["utils"] });

Просмотреть файл

@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
// @module: preserve
// @Filename: /node_modules/pkg/package.json
//// {
//// "name": "pkg",
//// "version": "1.0.0",
//// "exports": {
//// ".": "./index.js",
//// "./utils": "./utils.js"
//// }
//// }
// @Filename: /node_modules/pkg/utils.d.ts
//// export function add(a: number, b: number) {}
// @Filename: /node_modules/pkg/index.d.ts
//// export * from "./utils";
// @Filename: /src/index.ts
//// add/**/
verify.importFixModuleSpecifiers("", ["pkg", "pkg/utils"]);
verify.importFixModuleSpecifiers("", ["pkg/utils"], { autoImportSpecifierExcludeRegexes: ["^pkg$"] });

Просмотреть файл

@ -686,6 +686,7 @@ declare namespace FourSlashInterface {
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: readonly string[];
readonly autoImportSpecifierExcludeRegexes?: readonly string[];
readonly preferTypeOnlyAutoImports?: boolean;
readonly organizeImportsIgnoreCase?: "auto" | boolean;
readonly organizeImportsCollation?: "unicode" | "ordinal";