Skip to content

Commit eeeaadc

Browse files
ivanwonderthePunderWoman
authored andcommitted
fix(language-service): Support to resolve the re-export component. (#62585)
In the context of TypeScript (TS), a re-exported symbol is considered a distinct symbol. This means that a developer can choose to import either the re-exported symbol or the original symbol. However, in the context of Angular, the re-exported symbol is treated as the same component because it uses the same selector. This pull request will utilize the most recent re-export component file to resolve the module specifier. PR Close #62585
1 parent 6a6cb01 commit eeeaadc

5 files changed

Lines changed: 372 additions & 49 deletions

File tree

packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export enum PotentialImportMode {
145145
ForceDirect,
146146
}
147147

148+
export interface DirectiveModuleExportDetails {
149+
moduleSpecifier: string;
150+
exportName: string;
151+
}
152+
148153
export interface PotentialDirectiveModuleSpecifierResolver {
149-
resolve(toImport: Reference<ClassDeclaration>, importOn: ts.Node | null): string | undefined;
154+
resolve(
155+
toImport: Reference<ClassDeclaration>,
156+
importOn: ts.Node | null,
157+
): DirectiveModuleExportDetails | null;
150158
}

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

Lines changed: 131 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3131
import ts from 'typescript';
3232

@@ -66,6 +66,7 @@ import {
6666
isSymbolWithValueDeclaration,
6767
} from '../../util/src/typescript';
6868
import {
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
17871792
function 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+
}

packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,54 @@ export function isDirectiveDeclaration(node: ts.Node): node is ts.TypeNode | ts.
216216
hasExpressionIdentifier(sourceFile, node, ExpressionIdentifier.DIRECTIVE)
217217
);
218218
}
219+
220+
/**
221+
* Check if the lastSymbol is an alias of the firstSymbol. For example:
222+
*
223+
* The NewBarComponent is an alias of BarComponent.
224+
*
225+
* But the NotAliasBarComponent is not an alias of BarComponent, because
226+
* the NotAliasBarComponent is a new variable.
227+
*
228+
* This should work for most cases.
229+
*
230+
* https://github.com/microsoft/TypeScript/blob/9e20e032effad965567d4a1e1c30d5433b0a3332/src/compiler/checker.ts#L3638-L3652
231+
*
232+
* ```
233+
* // a.ts
234+
* export class BarComponent {};
235+
* // b.ts
236+
* export {BarComponent as NewBarComponent} from "./a";
237+
* // c.ts
238+
* import {BarComponent} from "./a"
239+
* const NotAliasBarComponent = BarComponent;
240+
* export {NotAliasBarComponent};
241+
* ```
242+
*/
243+
export function isSymbolAliasOf(
244+
firstSymbol: ts.Symbol,
245+
lastSymbol: ts.Symbol,
246+
typeChecker: ts.TypeChecker,
247+
): boolean {
248+
let currentSymbol: ts.Symbol | undefined = lastSymbol;
249+
250+
const seenSymbol: Set<ts.Symbol> = new Set();
251+
while (
252+
firstSymbol !== currentSymbol &&
253+
currentSymbol !== undefined &&
254+
currentSymbol.flags & ts.SymbolFlags.Alias
255+
) {
256+
if (seenSymbol.has(currentSymbol)) {
257+
break;
258+
}
259+
seenSymbol.add(currentSymbol);
260+
261+
currentSymbol = typeChecker.getImmediateAliasedSymbol(currentSymbol);
262+
263+
if (currentSymbol === firstSymbol) {
264+
return true;
265+
}
266+
}
267+
268+
return false;
269+
}

0 commit comments

Comments
 (0)