@@ -40,6 +40,9 @@ namespace ts.codefix {
4040 } ,
4141 } ) ;
4242
43+ /**
44+ * Computes multiple import additions to a file and writes them to a ChangeTracker.
45+ */
4346 export interface ImportAdder {
4447 hasFixes ( ) : boolean ;
4548 addImportFromDiagnostic : ( diagnostic : DiagnosticWithLocation , context : CodeFixContextBase ) => void ;
@@ -235,6 +238,47 @@ namespace ts.codefix {
235238 }
236239 }
237240
241+ /**
242+ * Computes module specifiers for multiple import additions to a file.
243+ */
244+ export interface ImportSpecifierResolver {
245+ getModuleSpecifierForBestExportInfo (
246+ exportInfo : readonly SymbolExportInfo [ ] ,
247+ symbolName : string ,
248+ position : number ,
249+ isValidTypeOnlyUseSite : boolean ,
250+ fromCacheOnly ?: boolean
251+ ) : { exportInfo ?: SymbolExportInfo , moduleSpecifier : string , computedWithoutCacheCount : number } | undefined ;
252+ }
253+
254+ export function createImportSpecifierResolver ( importingFile : SourceFile , program : Program , host : LanguageServiceHost , preferences : UserPreferences ) : ImportSpecifierResolver {
255+ const packageJsonImportFilter = createPackageJsonImportFilter ( importingFile , preferences , host ) ;
256+ const importMap = createExistingImportMap ( program . getTypeChecker ( ) , importingFile , program . getCompilerOptions ( ) ) ;
257+ return { getModuleSpecifierForBestExportInfo } ;
258+
259+ function getModuleSpecifierForBestExportInfo (
260+ exportInfo : readonly SymbolExportInfo [ ] ,
261+ symbolName : string ,
262+ position : number ,
263+ isValidTypeOnlyUseSite : boolean ,
264+ fromCacheOnly ?: boolean ,
265+ ) : { exportInfo ?: SymbolExportInfo , moduleSpecifier : string , computedWithoutCacheCount : number } | undefined {
266+ const { fixes, computedWithoutCacheCount } = getImportFixes (
267+ exportInfo ,
268+ { symbolName, position } ,
269+ isValidTypeOnlyUseSite ,
270+ /*useRequire*/ false ,
271+ program ,
272+ importingFile ,
273+ host ,
274+ preferences ,
275+ importMap ,
276+ fromCacheOnly ) ;
277+ const result = getBestFix ( fixes , importingFile , program , packageJsonImportFilter , host ) ;
278+ return result && { ...result , computedWithoutCacheCount } ;
279+ }
280+ }
281+
238282 // Sorted with the preferred fix coming first.
239283 const enum ImportFixKind { UseNamespace , JsdocTypeImport , AddToExisting , AddNew , PromoteTypeOnly }
240284 // These should not be combined as bitflags, but are given powers of 2 values to
@@ -394,32 +438,6 @@ namespace ts.codefix {
394438 }
395439 }
396440
397- export function getModuleSpecifierForBestExportInfo (
398- exportInfo : readonly SymbolExportInfo [ ] ,
399- symbolName : string ,
400- position : number ,
401- isValidTypeOnlyUseSite : boolean ,
402- importingFile : SourceFile ,
403- program : Program ,
404- host : LanguageServiceHost ,
405- preferences : UserPreferences ,
406- packageJsonImportFilter ?: PackageJsonImportFilter ,
407- fromCacheOnly ?: boolean ,
408- ) : { exportInfo ?: SymbolExportInfo , moduleSpecifier : string , computedWithoutCacheCount : number } | undefined {
409- const { fixes, computedWithoutCacheCount } = getImportFixes (
410- exportInfo ,
411- { symbolName, position } ,
412- isValidTypeOnlyUseSite ,
413- /*useRequire*/ false ,
414- program ,
415- importingFile ,
416- host ,
417- preferences ,
418- fromCacheOnly ) ;
419- const result = getBestFix ( fixes , importingFile , program , packageJsonImportFilter || createPackageJsonImportFilter ( importingFile , preferences , host ) , host ) ;
420- return result && { ...result , computedWithoutCacheCount } ;
421- }
422-
423441 function getImportFixes (
424442 exportInfos : readonly SymbolExportInfo [ ] ,
425443 useNamespaceInfo : {
@@ -433,10 +451,11 @@ namespace ts.codefix {
433451 sourceFile : SourceFile ,
434452 host : LanguageServiceHost ,
435453 preferences : UserPreferences ,
454+ importMap = createExistingImportMap ( program . getTypeChecker ( ) , sourceFile , program . getCompilerOptions ( ) ) ,
436455 fromCacheOnly ?: boolean ,
437456 ) : { computedWithoutCacheCount : number , fixes : readonly ImportFixWithModuleSpecifier [ ] } {
438457 const checker = program . getTypeChecker ( ) ;
439- const existingImports = flatMap ( exportInfos , info => getExistingImportDeclarations ( info , checker , sourceFile , program . getCompilerOptions ( ) ) ) ;
458+ const existingImports = flatMap ( exportInfos , importMap . getImportsForExportInfo ) ;
440459 const useNamespace = useNamespaceInfo && tryUseExistingNamespaceImport ( existingImports , useNamespaceInfo . symbolName , useNamespaceInfo . position , checker ) ;
441460 const addToExisting = tryAddToExistingImport ( existingImports , isValidTypeOnlyUseSite , checker , program . getCompilerOptions ( ) ) ;
442461 if ( addToExisting ) {
@@ -587,19 +606,34 @@ namespace ts.codefix {
587606 } ) ;
588607 }
589608
590- function getExistingImportDeclarations ( { moduleSymbol, exportKind, targetFlags, symbol } : SymbolExportInfo , checker : TypeChecker , importingFile : SourceFile , compilerOptions : CompilerOptions ) : readonly FixAddToExistingImportInfo [ ] {
591- // Can't use an es6 import for a type in JS.
592- if ( ! ( targetFlags & SymbolFlags . Value ) && isSourceFileJS ( importingFile ) ) return emptyArray ;
593- const importKind = getImportKind ( importingFile , exportKind , compilerOptions ) ;
594- return mapDefined ( importingFile . imports , ( moduleSpecifier ) : FixAddToExistingImportInfo | undefined => {
609+ function createExistingImportMap ( checker : TypeChecker , importingFile : SourceFile , compilerOptions : CompilerOptions ) {
610+ let importMap : MultiMap < SymbolId , AnyImportOrRequire > | undefined ;
611+ for ( const moduleSpecifier of importingFile . imports ) {
595612 const i = importFromModuleSpecifier ( moduleSpecifier ) ;
596613 if ( isVariableDeclarationInitializedToRequire ( i . parent ) ) {
597- return checker . resolveExternalModuleName ( moduleSpecifier ) === moduleSymbol ? { declaration : i . parent , importKind, symbol, targetFlags } : undefined ;
614+ const moduleSymbol = checker . resolveExternalModuleName ( moduleSpecifier ) ;
615+ if ( moduleSymbol ) {
616+ ( importMap ||= createMultiMap ( ) ) . add ( getSymbolId ( moduleSymbol ) , i . parent ) ;
617+ }
598618 }
599- if ( i . kind === SyntaxKind . ImportDeclaration || i . kind === SyntaxKind . ImportEqualsDeclaration ) {
600- return checker . getSymbolAtLocation ( moduleSpecifier ) === moduleSymbol ? { declaration : i , importKind, symbol, targetFlags } : undefined ;
619+ else if ( i . kind === SyntaxKind . ImportDeclaration || i . kind === SyntaxKind . ImportEqualsDeclaration ) {
620+ const moduleSymbol = checker . getSymbolAtLocation ( moduleSpecifier ) ;
621+ if ( moduleSymbol ) {
622+ ( importMap ||= createMultiMap ( ) ) . add ( getSymbolId ( moduleSymbol ) , i ) ;
623+ }
601624 }
602- } ) ;
625+ }
626+
627+ return {
628+ getImportsForExportInfo : ( { moduleSymbol, exportKind, targetFlags, symbol } : SymbolExportInfo ) : readonly FixAddToExistingImportInfo [ ] => {
629+ // Can't use an es6 import for a type in JS.
630+ if ( ! ( targetFlags & SymbolFlags . Value ) && isSourceFileJS ( importingFile ) ) return emptyArray ;
631+ const matchingDeclarations = importMap ?. get ( getSymbolId ( moduleSymbol ) ) ;
632+ if ( ! matchingDeclarations ) return emptyArray ;
633+ const importKind = getImportKind ( importingFile , exportKind , compilerOptions ) ;
634+ return matchingDeclarations . map ( declaration => ( { declaration, importKind, symbol, targetFlags } ) ) ;
635+ }
636+ } ;
603637 }
604638
605639 function shouldUseRequire ( sourceFile : SourceFile , program : Program ) : boolean {
0 commit comments