refactor(compiler-cli): decouple SymbolReference from AST nodes in template checker

To support the need to resolve symbols without full AST access (e.g. when using virtual files), this commit decouples `ReferenceSymbol` from `ts.ClassDeclaration`.

Changes:
- Updated `ReferenceSymbol.target` to use `SymbolReference` instead of `ts.ClassDeclaration`.
- Removed `getReferenceTargetNode()` from `SymbolDirectiveMeta` and transitioned to `getSymbolReference()`.
- Refactored `getTsSymbolOfReference` in `checker.ts` to handle `SymbolReference` and resolve it to a `ts.Symbol` using a position-optimized AST traversal. This avoids using the private `getTokenAtPosition` API and avoids full file scans by only traversing nodes containing the target position.

(cherry picked from commit c2f4b2af7c)
This commit is contained in:
Andrew Scott 2026-04-13 12:13:58 -07:00 committed by kirjs
parent bb8cdd9566
commit ff0af64ced
5 changed files with 81 additions and 20 deletions

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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))
);
}

View file

@ -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<TypeCheckingConfig>,