Add `autoImportSpecifierExcludeRegexes` preference (#59543)
This commit is contained in:
Родитель
1bb1d2a610
Коммит
09caaf60aa
|
@ -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";
|
||||
|
|
Загрузка…
Ссылка в новой задаче