@@ -26,7 +26,7 @@ import {
2626 WrappedNodeExpr ,
2727} from '@angular/compiler' ;
2828
29- import { isDirectiveDeclaration } from './ts_util' ;
29+ import { isDirectiveDeclaration , isSymbolAliasOf } from './ts_util' ;
3030
3131import ts from 'typescript' ;
3232
@@ -66,6 +66,7 @@ import {
6666 isSymbolWithValueDeclaration ,
6767} from '../../util/src/typescript' ;
6868import {
69+ DirectiveModuleExportDetails ,
6970 ElementSymbol ,
7071 FullSourceMapping ,
7172 GetPotentialAngularMetaOptions ,
@@ -1053,11 +1054,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
10531054 const cachedCompletionEntryInfos =
10541055 resultingDirectives . get ( directiveDecl . ref . node ) ?. tsCompletionEntryInfos ?? [ ] ;
10551056
1056- cachedCompletionEntryInfos . push ( {
1057- tsCompletionEntryData : data ,
1058- tsCompletionEntrySymbolFileName : symbolFileName ,
1059- tsCompletionEntrySymbolName : symbolName ,
1060- } ) ;
1057+ appendOrReplaceTsEntryInfo (
1058+ cachedCompletionEntryInfos ,
1059+ {
1060+ tsCompletionEntryData : data ,
1061+ tsCompletionEntrySymbolFileName : symbolFileName ,
1062+ tsCompletionEntrySymbolName : symbolName ,
1063+ } ,
1064+ this . programDriver . getProgram ( ) ,
1065+ ) ;
10611066
10621067 if ( resultingDirectives . has ( directiveDecl . ref . node ) ) {
10631068 const directiveInfo = resultingDirectives . get ( directiveDecl . ref . node ) ! ;
@@ -1283,37 +1288,37 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
12831288 */
12841289 let highestImportPriority = - 1 ;
12851290
1286- const collectImports = ( emit : PotentialImport | null , moduleSpecifier : string | undefined ) => {
1291+ const collectImports = (
1292+ emit : PotentialImport | null ,
1293+ moduleSpecifierDetail : DirectiveModuleExportDetails | null ,
1294+ ) => {
12871295 if ( emit === null ) {
12881296 return ;
12891297 }
12901298 imports . push ( {
12911299 ...emit ,
1292- moduleSpecifier : moduleSpecifier ?? emit . moduleSpecifier ,
1300+ moduleSpecifier : moduleSpecifierDetail ?. moduleSpecifier ?? emit . moduleSpecifier ,
1301+ symbolName : moduleSpecifierDetail ?. exportName ?? emit . symbolName ,
12931302 } ) ;
1294- if ( moduleSpecifier !== undefined && highestImportPriority === - 1 ) {
1303+ if ( moduleSpecifierDetail !== null && highestImportPriority === - 1 ) {
12951304 highestImportPriority = imports . length - 1 ;
12961305 }
12971306 } ;
12981307
12991308 if ( meta . isStandalone || importMode === PotentialImportMode . ForceDirect ) {
13001309 const emitted = this . emit ( PotentialImportKind . Standalone , toImport , inContext ) ;
1301- const moduleSpecifier = potentialDirectiveModuleSpecifierResolver ?. resolve (
1302- toImport ,
1303- inContext ,
1304- ) ;
1305- collectImports ( emitted , moduleSpecifier ) ;
1310+ const moduleSpecifierDetail =
1311+ potentialDirectiveModuleSpecifierResolver ?. resolve ( toImport , inContext ) ?? null ;
1312+ collectImports ( emitted , moduleSpecifierDetail ) ;
13061313 }
13071314
13081315 const exportingNgModules = this . ngModuleIndex . getNgModulesExporting ( meta . ref . node ) ;
13091316 if ( exportingNgModules !== null ) {
13101317 for ( const exporter of exportingNgModules ) {
13111318 const emittedRef = this . emit ( PotentialImportKind . NgModule , exporter , inContext ) ;
1312- const moduleSpecifier = potentialDirectiveModuleSpecifierResolver ?. resolve (
1313- exporter ,
1314- inContext ,
1315- ) ;
1316- collectImports ( emittedRef , moduleSpecifier ) ;
1319+ const moduleSpecifierDetail =
1320+ potentialDirectiveModuleSpecifierResolver ?. resolve ( exporter , inContext ) ?? null ;
1321+ collectImports ( emittedRef , moduleSpecifierDetail ) ;
13171322 }
13181323 }
13191324
@@ -1787,3 +1792,110 @@ type TsDeprecatedDiagnostics = Required<Pick<ts.DiagnosticWithLocation, 'reports
17871792function isDeprecatedDiagnostics ( diag : ts . DiagnosticWithLocation ) : diag is TsDeprecatedDiagnostics {
17881793 return diag . reportsDeprecated !== undefined ;
17891794}
1795+
1796+ /**
1797+ * Append the ts completion entry into the array only when the new entry's directive
1798+ * doesn't exist in the array.
1799+ *
1800+ * If the new entry's directive already exists, and the entry's symbol is the alias of
1801+ * the existing entry, the new entry will replace the existing entry.
1802+ *
1803+ */
1804+ function appendOrReplaceTsEntryInfo (
1805+ tsEntryInfos : TsCompletionEntryInfo [ ] ,
1806+ newTsEntryInfo : TsCompletionEntryInfo ,
1807+ program : ts . Program ,
1808+ ) {
1809+ const typeChecker = program . getTypeChecker ( ) ;
1810+ const newTsEntryInfoSymbol = getSymbolFromTsEntryInfo ( newTsEntryInfo , program ) ;
1811+ if ( newTsEntryInfoSymbol === null ) {
1812+ return ;
1813+ }
1814+
1815+ // Find the index of the first entry that has a matching type.
1816+ const matchedEntryIndex = tsEntryInfos . findIndex ( ( currentTsEntryInfo ) => {
1817+ const currentTsEntrySymbol = getSymbolFromTsEntryInfo ( currentTsEntryInfo , program ) ;
1818+ if ( currentTsEntrySymbol === null ) {
1819+ return false ;
1820+ }
1821+ return isSymbolTypeMatch ( currentTsEntrySymbol , newTsEntryInfoSymbol , typeChecker ) ;
1822+ } ) ;
1823+
1824+ if ( matchedEntryIndex === - 1 ) {
1825+ // No entry with a matching type was found, so append the new entry.
1826+ tsEntryInfos . push ( newTsEntryInfo ) ;
1827+ return ;
1828+ }
1829+
1830+ // An entry with a matching type was found at matchedEntryIndex.
1831+ const matchedEntry = tsEntryInfos [ matchedEntryIndex ] ;
1832+ const matchedEntrySymbol = getSymbolFromTsEntryInfo ( matchedEntry , program ) ;
1833+ if ( matchedEntrySymbol === null ) {
1834+ // Should not happen based on the findIndex condition, but check defensively.
1835+ return ;
1836+ }
1837+
1838+ // Check if the `matchedEntrySymbol` is an alias of the `newTsEntryInfoSymbol`.
1839+ if ( isSymbolAliasOf ( matchedEntrySymbol , newTsEntryInfoSymbol , typeChecker ) ) {
1840+ // The first type-matching entry is an alias, so replace it.
1841+ tsEntryInfos [ matchedEntryIndex ] = newTsEntryInfo ;
1842+ return ;
1843+ }
1844+
1845+ // The new entry's symbol is an alias of the existing entry's symbol.
1846+ // In this case, we prefer to keep the existing entry that was found first
1847+ // and do not replace it.
1848+ return ;
1849+ }
1850+
1851+ function getSymbolFromTsEntryInfo (
1852+ tsInfo : TsCompletionEntryInfo ,
1853+ program : ts . Program ,
1854+ ) : ts . Symbol | null {
1855+ const typeChecker = program . getTypeChecker ( ) ;
1856+ const sf = program . getSourceFile ( tsInfo . tsCompletionEntrySymbolFileName ) ;
1857+ if ( sf === undefined ) {
1858+ return null ;
1859+ }
1860+ const sfSymbol = typeChecker . getSymbolAtLocation ( sf ) ;
1861+ if ( sfSymbol === undefined ) {
1862+ return null ;
1863+ }
1864+
1865+ return (
1866+ typeChecker . tryGetMemberInModuleExports ( tsInfo . tsCompletionEntrySymbolName , sfSymbol ) ?? null
1867+ ) ;
1868+ }
1869+
1870+ function getFirstTypeDeclarationOfSymbol (
1871+ symbol : ts . Symbol ,
1872+ typeChecker : ts . TypeChecker ,
1873+ ) : ts . Declaration | undefined {
1874+ const type = typeChecker . getTypeOfSymbol ( symbol ) ;
1875+ return type . getSymbol ( ) ?. declarations ?. [ 0 ] ;
1876+ }
1877+
1878+ /**
1879+ * Check if the two symbols come from the same type node. For example:
1880+ *
1881+ * The `NewBarComponent`'s type node is the `BarComponent`.
1882+ *
1883+ * ```
1884+ * // a.ts
1885+ * export class BarComponent
1886+ *
1887+ * // b.ts
1888+ * import {BarComponent} from "./a"
1889+ * const NewBarComponent = BarComponent;
1890+ * export {NewBarComponent}
1891+ * ```
1892+ */
1893+ function isSymbolTypeMatch (
1894+ first : ts . Symbol ,
1895+ last : ts . Symbol ,
1896+ typeChecker : ts . TypeChecker ,
1897+ ) : boolean {
1898+ const firstTypeNode = getFirstTypeDeclarationOfSymbol ( first , typeChecker ) ;
1899+ const lastTypeNode = getFirstTypeDeclarationOfSymbol ( last , typeChecker ) ;
1900+ return firstTypeNode === lastTypeNode && firstTypeNode !== undefined ;
1901+ }
0 commit comments