mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
bb8cdd9566
commit
ff0af64ced
5 changed files with 81 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue