diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts index 385725f4a52..cb7adf56dd3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -21,7 +21,7 @@ import ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; import {SymbolWithValueDeclaration} from '../../util/src/typescript'; -import {PotentialDirective} from './scope'; +import {PotentialDirective, SymbolReference} from './scope'; export enum SymbolKind { Input, @@ -157,9 +157,9 @@ export interface ReferenceSymbol { * Depending on the type of the reference, this is one of the following: * - `TmplAstElement` when the local ref refers to the HTML element * - `TmplAstTemplate` when the ref refers to an `ng-template` - * - `ts.ClassDeclaration` when the local ref refers to a Directive instance (#ref="myExportAs") + * - `SymbolReference` when the local ref refers to a Directive instance (#ref="myExportAs") */ - target: TmplAstElement | TmplAstTemplate | ts.ClassDeclaration; + target: TmplAstElement | TmplAstTemplate | SymbolReference; /** * The node in the `TemplateAst` where the symbol is declared. That is, node for the `#ref` or diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 62ac1e04f1b..a5e0101399b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -88,6 +88,7 @@ import { PotentialImportKind, PotentialImportMode, PotentialPipe, + ReferenceSymbol, ProgramTypeCheckAdapter, SelectorlessComponentSymbol, SelectorlessDirectiveSymbol, @@ -363,12 +364,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const typeChecker = this.programDriver.getProgram().getTypeChecker(); + if ('kind' in symbol && symbol.kind === SymbolKind.Directive) { + const tsSymbol = this.getTsSymbolOfReference(symbol.ref, typeChecker); + if (tsSymbol) return tsSymbol; + } + if ('kind' in symbol && symbol.kind === SymbolKind.Reference) { - if (ts.isClassDeclaration(symbol.target as ts.Node)) { - const targetNode = symbol.target as ts.ClassDeclaration; - const tsSymbol = typeChecker.getSymbolAtLocation(targetNode.name ?? targetNode); - if (tsSymbol) return tsSymbol; - } + const tsSymbol = this.getTsSymbolOfReference(symbol.target, typeChecker); + if (tsSymbol) return tsSymbol; + if (ts.isCallExpression(node)) { return null; } @@ -410,6 +414,38 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return tsSymbol ?? typeChecker.getTypeAtLocation(node).symbol ?? null; } + private getTsSymbolOfReference( + target: SymbolReference | TmplAstElement | TmplAstTemplate, + typeChecker: ts.TypeChecker, + ): ts.Symbol | null { + if (!target || !('filePath' in target)) { + return null; + } + + const sf = this.programDriver.getProgram().getSourceFile(target.filePath); + if (!sf) { + return null; + } + + const visit = (node: ts.Node): ts.ClassDeclaration | null => { + if (node.pos <= target.position && target.position < node.end) { + if (ts.isClassDeclaration(node)) { + return node; + } + return ts.forEachChild(node, visit) ?? null; + } + return null; + }; + + const classDecl = ts.forEachChild(sf, visit) ?? null; + if (!classDecl) { + return null; + } + + const nameNode = classDecl.name ?? classDecl; + return typeChecker.getSymbolAtLocation(nameNode) ?? null; + } + getTemplate(component: ts.ClassDeclaration, optimizeFor?: OptimizeFor): TmplAstNode[] | null { const {data} = this.getLatestComponentState(component, optimizeFor); return data?.template ?? null; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index 09ebf00606d..f27a2ea04e2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -74,7 +74,6 @@ import {MaybeSourceFileWithOriginalFile, NgOriginalFile} from '../../program_dri export interface SymbolDirectiveMeta { getSymbolReference(): SymbolReference; getNgModule(): ClassDeclaration | null; - getReferenceTargetNode(): ts.ClassDeclaration | null; matchSource: MatchSource; isComponent: boolean; selector: string | null; @@ -604,10 +603,7 @@ export class SymbolBuilder { referenceVarLocation: referenceVarTcbLocation, }; } else { - const targetNode = target.directive.getReferenceTargetNode(); - if (targetNode === null) { - return null; - } + const targetNode = target.directive.getSymbolReference(); return { kind: SymbolKind.Reference, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts index d6d9f9e0d25..4885c4dc451 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts @@ -82,3 +82,12 @@ export function isSymbolAliasOf( return false; } + +/** + * Check if a node is a class declaration or the identifier of a class declaration. + */ +export function isClassDeclarationOrName(node: ts.Node): boolean { + return ( + ts.isClassDeclaration(node) || (ts.isIdentifier(node) && ts.isClassDeclaration(node.parent)) + ); +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index f8ef1df7bf6..fd0c599c50f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -48,6 +48,7 @@ import { SelectorlessDirectiveSymbol, Symbol, SymbolKind, + SymbolReference, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, @@ -253,7 +254,8 @@ runInEachFileSystem(() => { expect( program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), ).toEqual('TestDir'); - expect((symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); + expect((symbol.target as SymbolReference).filePath).toContain('dir.ts'); + assertTargetClassName(program, symbol.target, 'TestDir'); expect(symbol.declaration.name).toEqual('ref1'); } @@ -1064,12 +1066,14 @@ runInEachFileSystem(() => { const ref1Declaration = templateTypeChecker.getSymbolOfNode(nodes[0].references[0], cmp)!; assertReferenceSymbol(ref1Declaration); - expect((ref1Declaration.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); + expect((ref1Declaration.target as any).filePath).toContain('dir.ts'); + assertTargetClassName(program, ref1Declaration.target, 'TestDir'); expect((ref1Declaration.declaration as TmplAstReference).name).toEqual('myDir1'); const ref2Declaration = templateTypeChecker.getSymbolOfNode(nodes[1].references[0], cmp)!; assertReferenceSymbol(ref2Declaration); - expect((ref2Declaration.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); + expect((ref2Declaration.target as SymbolReference).filePath).toContain('dir.ts'); + assertTargetClassName(program, ref2Declaration.target, 'TestDir'); expect((ref2Declaration.declaration as TmplAstReference).name).toEqual('myDir2'); const dirValueSymbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[0].value, cmp)!; @@ -1087,12 +1091,14 @@ runInEachFileSystem(() => { const dir1Symbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[1].value, cmp)!; assertReferenceSymbol(dir1Symbol); - expect((dir1Symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); + expect((dir1Symbol.target as SymbolReference).filePath).toContain('dir.ts'); + assertTargetClassName(program, dir1Symbol.target, 'TestDir'); expect((dir1Symbol.declaration as TmplAstReference).name).toEqual('myDir1'); const dir2Symbol = templateTypeChecker.getSymbolOfNode(nodes[3].inputs[1].value, cmp)!; assertReferenceSymbol(dir2Symbol); - expect((dir2Symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); + expect((dir2Symbol.target as SymbolReference).filePath).toContain('dir.ts'); + assertTargetClassName(program, dir2Symbol.target, 'TestDir'); expect((dir2Symbol.declaration as TmplAstReference).name).toEqual('myDir2'); }); @@ -2676,7 +2682,8 @@ runInEachFileSystem(() => { const component = nodes[0] as TmplAstComponent; const symbol = templateTypeChecker.getSymbolOfNode(component.references[0], cmp)!; assertReferenceSymbol(symbol); - expect((symbol.target as ts.ClassDeclaration).name?.text).toBe('Dep'); + expect((symbol.target as any).filePath).toContain('dep.ts'); + assertTargetClassName(program, symbol.target, 'Dep'); expect(symbol.declaration.name).toBe('ref'); }); @@ -2699,7 +2706,8 @@ runInEachFileSystem(() => { const directive = (nodes[0] as TmplAstElement).directives[0]; const symbol = templateTypeChecker.getSymbolOfNode(directive.references[0], cmp)!; assertReferenceSymbol(symbol); - expect((symbol.target as ts.ClassDeclaration).name?.text).toBe('Dep'); + expect((symbol.target as SymbolReference).filePath).toContain('dep.ts'); + assertTargetClassName(program, symbol.target, 'Dep'); expect(symbol.declaration.name).toBe('ref'); }); }); @@ -3147,6 +3155,18 @@ function assertSelectorlessDirectiveSymbol( expect(tSymbol.kind).toEqual(SymbolKind.SelectorlessDirective); } +function assertTargetClassName(program: ts.Program, target: any, expectedName: string) { + const symbolRef = target as SymbolReference; + const sf = program.getSourceFile(symbolRef.filePath)!; + const classDecl = findNodeInFile( + sf, + (n): n is ts.ClassDeclaration => + ts.isClassDeclaration(n) && n.pos <= symbolRef.position && symbolRef.position < n.end, + ); + expect(classDecl).toBeTruthy(); + expect(classDecl!.name!.text).toEqual(expectedName); +} + export function setup( targets: TypeCheckingTarget[], config?: Partial,