diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index b397f29d74e..6eedc5b8e38 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -47,12 +47,14 @@ import { FutureSymbolExportInfo, getAllowSyntheticDefaultImports, getBaseFileName, + getDeclarationOfKind, getDefaultLikeExportInfo, getDirectoryPath, getEmitModuleKind, getEmitModuleResolutionKind, getEmitScriptTarget, getExportInfoMap, + getIsFileExcluded, getMeaningFromLocation, getNameForExportedSymbol, getOutputExtension, @@ -277,8 +279,14 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog const checker = program.getTypeChecker(); const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); const exportInfo = getAllExportInfoForSymbol(sourceFile, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); + if (!exportInfo) { + // If no exportInfo is found, this means export could not be resolved when we have filtered for autoImportFileExcludePatterns, + // so we should not generate an import. + Debug.assert(preferences.autoImportFileExcludePatterns?.length); + return; + } const useRequire = shouldUseRequire(sourceFile, program); - let fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); + let fix = getImportFixForSymbol(sourceFile, exportInfo, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); if (fix) { const localName = tryCast(referenceImport?.name, isIdentifier)?.text ?? symbolName; if ( @@ -859,9 +867,16 @@ function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAc function getAllExportInfoForSymbol(importingFile: SourceFile | FutureSourceFile, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { const getChecker = createGetChecker(program, host); + const isFileExcluded = preferences.autoImportFileExcludePatterns && getIsFileExcluded(host, preferences); + const mergedModuleSymbol = program.getTypeChecker().getMergedSymbol(moduleSymbol); + const moduleSourceFile = isFileExcluded && mergedModuleSymbol.declarations && getDeclarationOfKind(mergedModuleSymbol, SyntaxKind.SourceFile); + const moduleSymbolExcluded = moduleSourceFile && isFileExcluded(moduleSourceFile as SourceFile); return getExportInfoMap(importingFile, host, program, preferences, cancellationToken) .search(importingFile.path, preferCapitalized, name => name === symbolName, info => { - if (skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson)) === symbol && info.some(i => i.moduleSymbol === moduleSymbol || i.symbol.parent === moduleSymbol)) { + if ( + getChecker(info[0].isFromPackageJson).getMergedSymbol(skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson))) === symbol + && (moduleSymbolExcluded || info.some(i => i.moduleSymbol === moduleSymbol || i.symbol.parent === moduleSymbol)) + ) { return info; } }); diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 24d0c7b9b80..6bc8d84c4fe 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -426,12 +426,7 @@ export function forEachExternalModuleToImportFrom( cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void, ) { 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 = getSubPatternFromSpec(spec, "", "exclude"); - return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames) : undefined; - }); + const excludePatterns = preferences.autoImportFileExcludePatterns && getIsExcludedPatterns(preferences, useCaseSensitiveFileNames); forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), excludePatterns, host, (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); @@ -451,9 +446,33 @@ export function forEachExternalModuleToImportFrom( } } +function getIsExcludedPatterns(preferences: UserPreferences, useCaseSensitiveFileNames: boolean) { + return 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 = getSubPatternFromSpec(spec, "", "exclude"); + return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames) : undefined; + }); +} + function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], excludePatterns: readonly RegExp[] | undefined, host: LanguageServiceHost, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { + const isExcluded = excludePatterns && getIsExcluded(excludePatterns, host); + + for (const ambient of checker.getAmbientModules()) { + if (!ambient.name.includes("*") && !(excludePatterns && ambient.declarations?.every(d => isExcluded!(d.getSourceFile())))) { + cb(ambient, /*sourceFile*/ undefined); + } + } + for (const sourceFile of allSourceFiles) { + if (isExternalOrCommonJsModule(sourceFile) && !isExcluded?.(sourceFile)) { + cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile); + } + } +} + +function getIsExcluded(excludePatterns: readonly RegExp[], host: LanguageServiceHost) { const realpathsWithSymlinks = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); - const isExcluded = excludePatterns && (({ fileName, path }: SourceFile) => { + return (({ fileName, path }: SourceFile) => { if (excludePatterns.some(p => p.test(fileName))) return true; if (realpathsWithSymlinks?.size && pathContainsNodeModules(fileName)) { let dir = getDirectoryPath(fileName); @@ -467,17 +486,12 @@ function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly So } return false; }); +} - for (const ambient of checker.getAmbientModules()) { - if (!ambient.name.includes("*") && !(excludePatterns && ambient.declarations?.every(d => isExcluded!(d.getSourceFile())))) { - cb(ambient, /*sourceFile*/ undefined); - } - } - for (const sourceFile of allSourceFiles) { - if (isExternalOrCommonJsModule(sourceFile) && !isExcluded?.(sourceFile)) { - cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile); - } - } +/** @internal */ +export function getIsFileExcluded(host: LanguageServiceHost, preferences: UserPreferences) { + if (!preferences.autoImportFileExcludePatterns) return () => false; + return getIsExcluded(getIsExcludedPatterns(preferences, hostUsesCaseSensitiveFileNames(host)), host); } /** @internal */ diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns10.ts b/tests/cases/fourslash/autoImportFileExcludePatterns10.ts new file mode 100644 index 00000000000..a592f134f05 --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns10.ts @@ -0,0 +1,44 @@ +/// + +// @Filename: /src/vs/test.ts +//// import { Parts } from './parts'; +//// export class /**/Extended implements Parts { +//// } + +// @Filename: /src/vs/parts.ts +//// import { Event } from '../event/event'; +//// +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/thing.ts +//// import { Event } from './event/event'; +//// export { Event }; + +// @Filename: /src/a.ts +//// import './thing' +//// declare module './thing' { +//// interface Event { +//// c: string; +//// } +//// } + + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class Extended implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/thing.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns11.ts b/tests/cases/fourslash/autoImportFileExcludePatterns11.ts new file mode 100644 index 00000000000..44e214d78c3 --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns11.ts @@ -0,0 +1,43 @@ +/// + +// @Filename: /src/vs/test.ts +//// import { Parts } from './parts'; +//// export class /**/Extended implements Parts { +//// } + +// @Filename: /src/vs/parts.ts +//// import { Event } from '../thing'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/thing.ts +//// import { Event } from './event/event'; +//// export { Event }; + +// @Filename: /src/a.ts +//// import './thing' +//// declare module './thing' { +//// interface Event { +//// c: string; +//// } +//// } + + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class Extended implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/thing.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns12.ts b/tests/cases/fourslash/autoImportFileExcludePatterns12.ts new file mode 100644 index 00000000000..0c259e805ab --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns12.ts @@ -0,0 +1,42 @@ +/// + +// @Filename: /src/vs/test.ts +//// import { Parts } from './parts'; +//// export class /**/Extended implements Parts { +//// } + +// @Filename: /src/vs/parts.ts +//// import { Event } from '../thing'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/thing.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +// @Filename: /src/a.ts +//// import './thing' +//// declare module './thing' { +//// interface Event { +//// c: string; +//// } +//// } + +// In this test, `Event` is incorrectly imported in `thing.ts` +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Parts } from './parts'; +export class Extended implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/thing.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns13.ts b/tests/cases/fourslash/autoImportFileExcludePatterns13.ts new file mode 100644 index 00000000000..544262eb62a --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns13.ts @@ -0,0 +1,44 @@ +/// + +// @Filename: /src/vs/test.ts +//// import { Parts } from './parts'; +//// export class /**/Extended implements Parts { +//// } + +// @Filename: /src/vs/parts.ts +//// import { Event } from '../event/event'; +//// +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/thing.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +// @Filename: /src/a.ts +//// import './thing' +//// declare module './thing' { +//// interface Event { +//// c: string; +//// } +//// } + +// In this test, `Event` is incorrectly imported in `thing.ts` +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class Extended implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/thing.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns4.ts b/tests/cases/fourslash/autoImportFileExcludePatterns4.ts new file mode 100644 index 00000000000..c01c65932d9 --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns4.ts @@ -0,0 +1,33 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns5.ts b/tests/cases/fourslash/autoImportFileExcludePatterns5.ts new file mode 100644 index 00000000000..54e1b7a9dcf --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns5.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +// @Filename: /src/vs/workbench/canImport.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from './canImport'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns6.ts b/tests/cases/fourslash/autoImportFileExcludePatterns6.ts new file mode 100644 index 00000000000..0dd2ba7ccdf --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns6.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from './canImport'; +//// export { Event }; + +// @Filename: /src/vs/workbench/canImport.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from './canImport'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench.ts"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns7.ts b/tests/cases/fourslash/autoImportFileExcludePatterns7.ts new file mode 100644 index 00000000000..1782dc9721d --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns7.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +// @Filename: /src/vs/workbench/workbench2.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench*"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns8.ts b/tests/cases/fourslash/autoImportFileExcludePatterns8.ts new file mode 100644 index 00000000000..81a93ebde1b --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns8.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from './workbench2'; +//// export { Event }; + +// @Filename: /src/vs/workbench/workbench2.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../event/event'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench*"], + } +}); diff --git a/tests/cases/fourslash/autoImportFileExcludePatterns9.ts b/tests/cases/fourslash/autoImportFileExcludePatterns9.ts new file mode 100644 index 00000000000..bb0def4d5ad --- /dev/null +++ b/tests/cases/fourslash/autoImportFileExcludePatterns9.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /src/vs/workbench/test.ts +//// import { Parts } from './parts'; +//// export class /**/EditorParts implements Parts { } + +// @Filename: /src/vs/event/event.ts +//// export interface Event { +//// (): string; +//// } + +// @Filename: /src/vs/workbench/parts.ts +//// import { Event } from '../event/event'; +//// export interface Parts { +//// readonly options: Event; +//// } + +// @Filename: /src/vs/workbench/workbench.ts +//// import { Event } from '../event/event'; +//// export { Event }; + +// @Filename: /src/vs/test.ts +//// import { Event } from './event/event'; +//// export { Event }; + +verify.codeFix({ + description: "Implement interface 'Parts'", + newFileContent: +`import { Event } from '../test'; +import { Parts } from './parts'; +export class EditorParts implements Parts { + options: Event; +}`, + preferences: { + autoImportFileExcludePatterns: ["src/vs/workbench/workbench*"], + } +});