@@ -81,9 +81,9 @@ namespace ts.codefix {
8181 const symbolName = getNameForExportedSymbol ( exportedSymbol , getEmitScriptTarget ( compilerOptions ) ) ;
8282 const checker = program . getTypeChecker ( ) ;
8383 const symbol = checker . getMergedSymbol ( skipAlias ( exportedSymbol , checker ) ) ;
84- const exportInfos = getAllReExportingModules ( sourceFile , symbol , moduleSymbol , symbolName , /*isJsxTagName*/ false , host , program , preferences , useAutoImportProvider ) ;
84+ const exportInfo = getAllReExportingModules ( sourceFile , symbol , moduleSymbol , symbolName , /*isJsxTagName*/ false , host , program , preferences , useAutoImportProvider ) ;
8585 const useRequire = shouldUseRequire ( sourceFile , program ) ;
86- const fix = getImportFixForSymbol ( sourceFile , exportInfos , moduleSymbol , symbolName , program , /*position*/ undefined , ! ! isValidTypeOnlyUseSite , useRequire , host , preferences ) ;
86+ const fix = getImportFixForSymbol ( sourceFile , exportInfo , moduleSymbol , symbolName , program , /*position*/ undefined , ! ! isValidTypeOnlyUseSite , useRequire , host , preferences ) ;
8787 if ( fix ) {
8888 addImport ( { fixes : [ fix ] , symbolName, errorIdentifierText : undefined } ) ;
8989 }
@@ -248,32 +248,35 @@ namespace ts.codefix {
248248 }
249249 type ImportFix = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport | FixPromoteTypeOnlyImport ;
250250 type ImportFixWithModuleSpecifier = FixUseNamespaceImport | FixAddJsdocTypeImport | FixAddToExistingImport | FixAddNewImport ;
251- interface FixUseNamespaceImport {
251+
252+ // Properties are be undefined if fix is derived from an existing import
253+ interface ImportFixBase {
254+ readonly isReExport ?: boolean ;
255+ readonly exportInfo ?: SymbolExportInfo ;
256+ readonly moduleSpecifier : string ;
257+ }
258+ interface FixUseNamespaceImport extends ImportFixBase {
252259 readonly kind : ImportFixKind . UseNamespace ;
253260 readonly namespacePrefix : string ;
254261 readonly position : number ;
255- readonly moduleSpecifier : string ;
256262 }
257- interface FixAddJsdocTypeImport {
263+ interface FixAddJsdocTypeImport extends ImportFixBase {
258264 readonly kind : ImportFixKind . JsdocTypeImport ;
259- readonly moduleSpecifier : string ;
260265 readonly position : number ;
266+ readonly isReExport : boolean ;
261267 readonly exportInfo : SymbolExportInfo ;
262268 }
263- interface FixAddToExistingImport {
269+ interface FixAddToExistingImport extends ImportFixBase {
264270 readonly kind : ImportFixKind . AddToExisting ;
265271 readonly importClauseOrBindingPattern : ImportClause | ObjectBindingPattern ;
266- readonly moduleSpecifier : string ;
267272 readonly importKind : ImportKind . Default | ImportKind . Named ;
268273 readonly addAsTypeOnly : AddAsTypeOnly ;
269274 }
270- interface FixAddNewImport {
275+ interface FixAddNewImport extends ImportFixBase {
271276 readonly kind : ImportFixKind . AddNew ;
272- readonly moduleSpecifier : string ;
273277 readonly importKind : ImportKind ;
274278 readonly addAsTypeOnly : AddAsTypeOnly ;
275279 readonly useRequire : boolean ;
276- readonly exportInfo ?: SymbolExportInfo ;
277280 }
278281 interface FixPromoteTypeOnlyImport {
279282 readonly kind : ImportFixKind . PromoteTypeOnly ;
@@ -331,7 +334,7 @@ namespace ts.codefix {
331334 function getImportFixForSymbol ( sourceFile : SourceFile , exportInfos : readonly SymbolExportInfo [ ] , moduleSymbol : Symbol , symbolName : string , program : Program , position : number | undefined , isValidTypeOnlyUseSite : boolean , useRequire : boolean , host : LanguageServiceHost , preferences : UserPreferences ) {
332335 Debug . assert ( exportInfos . some ( info => info . moduleSymbol === moduleSymbol || info . symbol . parent === moduleSymbol ) , "Some exportInfo should match the specified moduleSymbol" ) ;
333336 const packageJsonImportFilter = createPackageJsonImportFilter ( sourceFile , preferences , host ) ;
334- return getBestFix ( getImportFixes ( exportInfos , symbolName , position , isValidTypeOnlyUseSite , useRequire , program , sourceFile , host , preferences ) , sourceFile , program , packageJsonImportFilter ) ;
337+ return getBestFix ( getImportFixes ( exportInfos , symbolName , position , isValidTypeOnlyUseSite , useRequire , program , sourceFile , host , preferences ) , sourceFile , program , packageJsonImportFilter , host ) ;
335338 }
336339
337340 function codeFixActionToCodeAction ( { description, changes, commands } : CodeFixAction ) : CodeAction {
@@ -410,7 +413,7 @@ namespace ts.codefix {
410413 host ,
411414 preferences ,
412415 fromCacheOnly ) ;
413- const result = getBestFix ( fixes , importingFile , program , packageJsonImportFilter || createPackageJsonImportFilter ( importingFile , preferences , host ) ) ;
416+ const result = getBestFix ( fixes , importingFile , program , packageJsonImportFilter || createPackageJsonImportFilter ( importingFile , preferences , host ) , host ) ;
414417 return result && { ...result , computedWithoutCacheCount } ;
415418 }
416419
@@ -606,7 +609,7 @@ namespace ts.codefix {
606609 position : number | undefined ,
607610 isValidTypeOnlyUseSite : boolean ,
608611 useRequire : boolean ,
609- moduleSymbols : readonly SymbolExportInfo [ ] ,
612+ exportInfo : readonly SymbolExportInfo [ ] ,
610613 host : LanguageServiceHost ,
611614 preferences : UserPreferences ,
612615 fromCacheOnly ?: boolean ,
@@ -620,7 +623,7 @@ namespace ts.codefix {
620623 : ( moduleSymbol : Symbol , checker : TypeChecker ) => moduleSpecifiers . getModuleSpecifiersWithCacheInfo ( moduleSymbol , checker , compilerOptions , sourceFile , moduleSpecifierResolutionHost , preferences ) ;
621624
622625 let computedWithoutCacheCount = 0 ;
623- const fixes = flatMap ( moduleSymbols , exportInfo => {
626+ const fixes = flatMap ( exportInfo , ( exportInfo , i ) => {
624627 const checker = getChecker ( exportInfo . isFromPackageJson ) ;
625628 const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers ( exportInfo . moduleSymbol , checker ) ;
626629 const importedSymbolHasValueMeaning = ! ! ( exportInfo . targetFlags & SymbolFlags . Value ) ;
@@ -629,14 +632,15 @@ namespace ts.codefix {
629632 return moduleSpecifiers ?. map ( ( moduleSpecifier ) : FixAddNewImport | FixAddJsdocTypeImport =>
630633 // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
631634 ! importedSymbolHasValueMeaning && isJs && position !== undefined
632- ? { kind : ImportFixKind . JsdocTypeImport , moduleSpecifier, position, exportInfo }
635+ ? { kind : ImportFixKind . JsdocTypeImport , moduleSpecifier, position, exportInfo, isReExport : i > 0 }
633636 : {
634637 kind : ImportFixKind . AddNew ,
635638 moduleSpecifier,
636639 importKind : getImportKind ( sourceFile , exportInfo . exportKind , compilerOptions ) ,
637640 useRequire,
638641 addAsTypeOnly,
639642 exportInfo,
643+ isReExport : i > 0 ,
640644 }
641645 ) ;
642646 } ) ;
@@ -675,7 +679,7 @@ namespace ts.codefix {
675679 }
676680 }
677681
678- interface FixesInfo { readonly fixes : readonly ImportFix [ ] ; readonly symbolName : string ; readonly errorIdentifierText : string | undefined ; }
682+ interface FixesInfo { readonly fixes : readonly ImportFix [ ] , readonly symbolName : string , readonly errorIdentifierText : string | undefined }
679683 function getFixesInfo ( context : CodeFixContextBase , errorCode : number , pos : number , useAutoImportProvider : boolean ) : FixesInfo | undefined {
680684 const symbolToken = getTokenAtPosition ( context . sourceFile , pos ) ;
681685 let info ;
@@ -695,39 +699,73 @@ namespace ts.codefix {
695699 }
696700
697701 const packageJsonImportFilter = createPackageJsonImportFilter ( context . sourceFile , context . preferences , context . host ) ;
698- return info && { ...info , fixes : sortFixes ( info . fixes , context . sourceFile , context . program , packageJsonImportFilter ) } ;
702+ return info && { ...info , fixes : sortFixes ( info . fixes , context . sourceFile , context . program , packageJsonImportFilter , context . host ) } ;
699703 }
700704
701- function sortFixes ( fixes : readonly ImportFixWithModuleSpecifier [ ] , sourceFile : SourceFile , program : Program , packageJsonImportFilter : PackageJsonImportFilter ) : readonly ImportFixWithModuleSpecifier [ ] {
702- return sort ( fixes , ( a , b ) => compareValues ( a . kind , b . kind ) || compareModuleSpecifiers ( a , b , sourceFile , program , packageJsonImportFilter . allowsImportingSpecifier ) ) ;
705+ function sortFixes ( fixes : readonly ImportFixWithModuleSpecifier [ ] , sourceFile : SourceFile , program : Program , packageJsonImportFilter : PackageJsonImportFilter , host : LanguageServiceHost ) : readonly ImportFixWithModuleSpecifier [ ] {
706+ const _toPath = ( fileName : string ) => toPath ( fileName , host . getCurrentDirectory ( ) , hostGetCanonicalFileName ( host ) ) ;
707+ return sort ( fixes , ( a , b ) => compareValues ( a . kind , b . kind ) || compareModuleSpecifiers ( a , b , sourceFile , program , packageJsonImportFilter . allowsImportingSpecifier , _toPath ) ) ;
703708 }
704709
705- function getBestFix < T extends ImportFixWithModuleSpecifier > ( fixes : readonly T [ ] , sourceFile : SourceFile , program : Program , packageJsonImportFilter : PackageJsonImportFilter ) : T | undefined {
710+ function getBestFix ( fixes : readonly ImportFixWithModuleSpecifier [ ] , sourceFile : SourceFile , program : Program , packageJsonImportFilter : PackageJsonImportFilter , host : LanguageServiceHost ) : ImportFixWithModuleSpecifier | undefined {
706711 if ( ! some ( fixes ) ) return ;
707712 // These will always be placed first if available, and are better than other kinds
708713 if ( fixes [ 0 ] . kind === ImportFixKind . UseNamespace || fixes [ 0 ] . kind === ImportFixKind . AddToExisting ) {
709714 return fixes [ 0 ] ;
710715 }
716+
711717 return fixes . reduce ( ( best , fix ) =>
712718 // Takes true branch of conditional if `fix` is better than `best`
713- compareModuleSpecifiers ( fix , best , sourceFile , program , packageJsonImportFilter . allowsImportingSpecifier ) === Comparison . LessThan ? fix : best
719+ compareModuleSpecifiers (
720+ fix ,
721+ best ,
722+ sourceFile ,
723+ program ,
724+ packageJsonImportFilter . allowsImportingSpecifier ,
725+ fileName => toPath ( fileName , host . getCurrentDirectory ( ) , hostGetCanonicalFileName ( host ) ) ,
726+ ) === Comparison . LessThan ? fix : best
714727 ) ;
715728 }
716729
717730 /** @returns `Comparison.LessThan` if `a` is better than `b`. */
718- function compareModuleSpecifiers ( a : ImportFixWithModuleSpecifier , b : ImportFixWithModuleSpecifier , importingFile : SourceFile , program : Program , allowsImportingSpecifier : ( specifier : string ) => boolean ) : Comparison {
731+ function compareModuleSpecifiers (
732+ a : ImportFixWithModuleSpecifier ,
733+ b : ImportFixWithModuleSpecifier ,
734+ importingFile : SourceFile ,
735+ program : Program ,
736+ allowsImportingSpecifier : ( specifier : string ) => boolean ,
737+ toPath : ( fileName : string ) => Path ,
738+ ) : Comparison {
719739 if ( a . kind !== ImportFixKind . UseNamespace && b . kind !== ImportFixKind . UseNamespace ) {
720740 return compareBooleans ( allowsImportingSpecifier ( b . moduleSpecifier ) , allowsImportingSpecifier ( a . moduleSpecifier ) )
721741 || compareNodeCoreModuleSpecifiers ( a . moduleSpecifier , b . moduleSpecifier , importingFile , program )
722- || compareBooleans ( isOnlyDotsAndSlashes ( a . moduleSpecifier ) , isOnlyDotsAndSlashes ( b . moduleSpecifier ) )
742+ || compareBooleans (
743+ isFixPossiblyReExportingImportingFile ( a , importingFile , program . getCompilerOptions ( ) , toPath ) ,
744+ isFixPossiblyReExportingImportingFile ( b , importingFile , program . getCompilerOptions ( ) , toPath ) )
723745 || compareNumberOfDirectorySeparators ( a . moduleSpecifier , b . moduleSpecifier ) ;
724746 }
725747 return Comparison . EqualTo ;
726748 }
727749
728- const notDotOrSlashPattern = / [ ^ . \/ ] / ;
729- function isOnlyDotsAndSlashes ( path : string ) {
730- return ! notDotOrSlashPattern . test ( path ) ;
750+ // This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export.
751+ // E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`.
752+ // This can produce false positives or negatives if re-exports cross into sibling directories
753+ // (e.g. `export * from "../whatever"`) or are not named "index" (we don't even try to consider
754+ // this if we're in a resolution mode where you can't drop trailing "/index" from paths).
755+ function isFixPossiblyReExportingImportingFile ( fix : ImportFixWithModuleSpecifier , importingFile : SourceFile , compilerOptions : CompilerOptions , toPath : ( fileName : string ) => Path ) : boolean {
756+ if ( fix . isReExport &&
757+ fix . exportInfo ?. moduleFileName &&
758+ getEmitModuleResolutionKind ( compilerOptions ) === ModuleResolutionKind . NodeJs &&
759+ isIndexFileName ( fix . exportInfo . moduleFileName )
760+ ) {
761+ const reExportDir = toPath ( getDirectoryPath ( fix . exportInfo . moduleFileName ) ) ;
762+ return startsWith ( ( importingFile . path ) , reExportDir ) ;
763+ }
764+ return false ;
765+ }
766+
767+ function isIndexFileName ( fileName : string ) {
768+ return getBaseFileName ( fileName , [ ".js" , ".jsx" , ".d.ts" , ".ts" , ".tsx" ] , /*ignoreCase*/ true ) === "index" ;
731769 }
732770
733771 function compareNodeCoreModuleSpecifiers ( a : string , b : string , importingFile : SourceFile , program : Program ) : Comparison {
@@ -742,9 +780,9 @@ namespace ts.codefix {
742780 if ( ! umdSymbol ) return undefined ;
743781 const symbol = checker . getAliasedSymbol ( umdSymbol ) ;
744782 const symbolName = umdSymbol . name ;
745- const exportInfos : readonly SymbolExportInfo [ ] = [ { symbol : umdSymbol , moduleSymbol : symbol , moduleFileName : undefined , exportKind : ExportKind . UMD , targetFlags : symbol . flags , isFromPackageJson : false } ] ;
783+ const exportInfo : readonly SymbolExportInfo [ ] = [ { symbol : umdSymbol , moduleSymbol : symbol , moduleFileName : undefined , exportKind : ExportKind . UMD , targetFlags : symbol . flags , isFromPackageJson : false } ] ;
746784 const useRequire = shouldUseRequire ( sourceFile , program ) ;
747- const fixes = getImportFixes ( exportInfos , symbolName , isIdentifier ( token ) ? token . getStart ( sourceFile ) : undefined , /*isValidTypeOnlyUseSite*/ false , useRequire , program , sourceFile , host , preferences ) ;
785+ const fixes = getImportFixes ( exportInfo , symbolName , isIdentifier ( token ) ? token . getStart ( sourceFile ) : undefined , /*isValidTypeOnlyUseSite*/ false , useRequire , program , sourceFile , host , preferences ) ;
748786 return { fixes, symbolName, errorIdentifierText : tryCast ( token , isIdentifier ) ?. text } ;
749787 }
750788 function getUmdSymbol ( token : Node , checker : TypeChecker ) : Symbol | undefined {
@@ -814,8 +852,8 @@ namespace ts.codefix {
814852
815853 const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite ( symbolToken ) ;
816854 const useRequire = shouldUseRequire ( sourceFile , program ) ;
817- const exportInfos = getExportInfos ( symbolName , isJSXTagName ( symbolToken ) , getMeaningFromLocation ( symbolToken ) , cancellationToken , sourceFile , program , useAutoImportProvider , host , preferences ) ;
818- const fixes = arrayFrom ( flatMapIterator ( exportInfos . entries ( ) , ( [ _ , exportInfos ] ) =>
855+ const exportInfo = getExportInfos ( symbolName , isJSXTagName ( symbolToken ) , getMeaningFromLocation ( symbolToken ) , cancellationToken , sourceFile , program , useAutoImportProvider , host , preferences ) ;
856+ const fixes = arrayFrom ( flatMapIterator ( exportInfo . entries ( ) , ( [ _ , exportInfos ] ) =>
819857 getImportFixes ( exportInfos , symbolName , symbolToken . getStart ( sourceFile ) , isValidTypeOnlyUseSite , useRequire , program , sourceFile , host , preferences ) ) ) ;
820858 return { fixes, symbolName, errorIdentifierText : symbolToken . text } ;
821859 }
0 commit comments