Add TS Server option to exclude files from auto-imports (#49578)

* Basic functionality

* Add tests

* Add test for ambient modules

* Add to protocol
This commit is contained in:
Andrew Branch 2022-06-17 14:39:51 -07:00 committed by GitHub
parent 1213c35d57
commit 7e7c53961a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 171 additions and 11 deletions

View File

@ -8990,6 +8990,7 @@ namespace ts {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
}
/** Represents a bigint literal value without requiring bigint support */

View File

@ -3469,6 +3469,7 @@ namespace ts.server.protocol {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];
}
export interface CompilerOptions {

View File

@ -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();

View File

@ -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(

View File

@ -336,32 +336,42 @@ 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 => {
// 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(), 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);
}
}
}
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 +392,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();

View File

@ -4138,6 +4138,7 @@ declare namespace ts {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
}
/** Represents a bigint literal value without requiring bigint support */
export interface PseudoBigInt {
@ -9769,6 +9770,7 @@ declare namespace ts.server.protocol {
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];
}
interface CompilerOptions {
allowJs?: boolean;

View File

@ -4138,6 +4138,7 @@ declare namespace ts {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
}
/** Represents a bigint literal value without requiring bigint support */
export interface PseudoBigInt {

View File

@ -0,0 +1,22 @@
/// <reference path="fourslash.ts"/>
// @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 });

View File

@ -0,0 +1,39 @@
/// <reference path="fourslash.ts" />
// @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.*"] });

View File

@ -0,0 +1,52 @@
/// <reference path="fourslash.ts" />
// @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*"],
}
});

View File

@ -662,6 +662,7 @@ declare namespace FourSlashInterface {
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: readonly string[];
}
interface InlayHintsOptions extends UserPreferences {
readonly includeInlayParameterNameHints?: "none" | "literals" | "all";

View File

@ -0,0 +1,31 @@
/// <reference path="../fourslash.ts"/>
// @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 });