feat(compiler-cli): add ability to get symbol of reference or variable (#38618)
Adds `TemplateTypeChecker` operation to retrieve the `Symbol` of a `TmplAstVariable` or `TmplAstReference` in a template. Sometimes we need to traverse an intermediate variable declaration to arrive at the correct `ts.Symbol`. For example, loop variables are declared using an intermediate: ``` <div *ngFor="let user of users"> {{user.name}} </div> ``` Getting the symbol of user here (from the expression) is tricky, because the TCB looks like: ``` var _t0 = ...; // type of NgForOf var _t1: any; // context of embedded view for NgForOf structural directive if (NgForOf.ngTemplateContextGuard(_t0, _t1)) { // _t1 is now NgForOfContext<...> var _t2 = _t1.$implicit; // let user = '$implicit' _t2.name; // user.name expression } ``` Just getting the `ts.Expression` for the `AST` node `PropRead(ImplicitReceiver, 'user')` via the sourcemaps will yield the `_t2` expression. This function recognizes that `_t2` is a variable declared locally in the TCB, and actually fetch the `ts.Symbol` of its initializer. These special handlings show the versatility of the `Symbol` interface defined in the API. With this, when we encounter a template variable, we can provide the declaration node, as well as specific information about the variable instance, such as the `ts.Type` and `ts.Symbol`. PR Close #38618
This commit is contained in:
parent
f56ece4fdc
commit
19598b47ca
|
@ -6,12 +6,12 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteSourceSpan, AST, ASTWithSource, BindingPipe, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import {AbsoluteSourceSpan, AST, ASTWithSource, BindingPipe, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../file_system';
|
||||
import {isAssignment} from '../../util/src/typescript';
|
||||
import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo} from '../api';
|
||||
import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, VariableSymbol} from '../api';
|
||||
|
||||
import {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments';
|
||||
import {TemplateData} from './context';
|
||||
|
@ -28,6 +28,7 @@ export class SymbolBuilder {
|
|||
private readonly typeCheckBlock: ts.Node, private readonly templateData: TemplateData) {}
|
||||
|
||||
getSymbol(node: TmplAstTemplate|TmplAstElement): TemplateSymbol|ElementSymbol|null;
|
||||
getSymbol(node: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null;
|
||||
getSymbol(node: AST|TmplAstNode): Symbol|null;
|
||||
getSymbol(node: AST|TmplAstNode): Symbol|null {
|
||||
if (node instanceof TmplAstBoundAttribute) {
|
||||
|
@ -40,6 +41,10 @@ export class SymbolBuilder {
|
|||
return this.getSymbolOfElement(node);
|
||||
} else if (node instanceof TmplAstTemplate) {
|
||||
return this.getSymbolOfAstTemplate(node);
|
||||
} else if (node instanceof TmplAstVariable) {
|
||||
return this.getSymbolOfVariable(node);
|
||||
} else if (node instanceof TmplAstReference) {
|
||||
return this.getSymbolOfReference(node);
|
||||
} else if (node instanceof AST) {
|
||||
return this.getSymbolOfTemplateExpression(node);
|
||||
}
|
||||
|
@ -226,11 +231,70 @@ export class SymbolBuilder {
|
|||
};
|
||||
}
|
||||
|
||||
private getSymbolOfTemplateExpression(expression: AST): ExpressionSymbol|null {
|
||||
private getSymbolOfVariable(variable: TmplAstVariable): VariableSymbol|null {
|
||||
const node = findFirstMatchingNode(
|
||||
this.typeCheckBlock, {withSpan: variable.sourceSpan, filter: ts.isVariableDeclaration});
|
||||
if (node === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expressionSymbol = this.getSymbolOfVariableDeclaration(node);
|
||||
if (expressionSymbol === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {...expressionSymbol, kind: SymbolKind.Variable, declaration: variable};
|
||||
}
|
||||
|
||||
private getSymbolOfReference(ref: TmplAstReference): ReferenceSymbol|null {
|
||||
const target = this.templateData.boundTarget.getReferenceTarget(ref);
|
||||
// Find the node for the reference declaration, i.e. `var _t2 = _t1;`
|
||||
let node = findFirstMatchingNode(
|
||||
this.typeCheckBlock, {withSpan: ref.sourceSpan, filter: ts.isVariableDeclaration});
|
||||
if (node === null || target === null || node.initializer === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO(atscott): Shim location will need to be adjusted
|
||||
const symbol = this.getSymbolOfTsNode(node.name);
|
||||
if (symbol === null || symbol.tsSymbol === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) {
|
||||
return {
|
||||
...symbol,
|
||||
tsSymbol: symbol.tsSymbol,
|
||||
kind: SymbolKind.Reference,
|
||||
target,
|
||||
declaration: ref,
|
||||
};
|
||||
} else {
|
||||
if (!ts.isClassDeclaration(target.directive.ref.node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...symbol,
|
||||
kind: SymbolKind.Reference,
|
||||
tsSymbol: symbol.tsSymbol,
|
||||
declaration: ref,
|
||||
target: target.directive.ref.node,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getSymbolOfTemplateExpression(expression: AST): VariableSymbol|ReferenceSymbol
|
||||
|ExpressionSymbol|null {
|
||||
if (expression instanceof ASTWithSource) {
|
||||
expression = expression.ast;
|
||||
}
|
||||
|
||||
const expressionTarget = this.templateData.boundTarget.getExpressionTarget(expression);
|
||||
if (expressionTarget !== null) {
|
||||
return this.getSymbol(expressionTarget);
|
||||
}
|
||||
|
||||
let node = findFirstMatchingNode(
|
||||
this.typeCheckBlock,
|
||||
{withSpan: expression.sourceSpan, filter: (n: ts.Node): n is ts.Node => true});
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig} from '../api';
|
||||
import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api';
|
||||
|
||||
import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
||||
|
||||
|
@ -79,6 +79,59 @@ runInEachFileSystem(() => {
|
|||
templateNode = getAstTemplates(templateTypeChecker, cmp)[0];
|
||||
});
|
||||
|
||||
it('should get symbol for variables at the declaration', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(templateNode.variables[0], cmp)!;
|
||||
assertVariableSymbol(symbol);
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any');
|
||||
expect(symbol.declaration.name).toEqual('contextFoo');
|
||||
});
|
||||
|
||||
it('should get symbol for variables when used', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(
|
||||
(templateNode.children[0] as TmplAstTemplate).inputs[0].value, cmp)!;
|
||||
assertVariableSymbol(symbol);
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any');
|
||||
expect(symbol.declaration.name).toEqual('contextFoo');
|
||||
});
|
||||
|
||||
it('should get a symbol for local ref which refers to a directive', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(templateNode.references[1], cmp)!;
|
||||
assertReferenceSymbol(symbol);
|
||||
assertDirectiveReference(symbol);
|
||||
});
|
||||
|
||||
it('should get a symbol for usage local ref which refers to a directive', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(
|
||||
(templateNode.children[0] as TmplAstTemplate).inputs[2].value, cmp)!;
|
||||
assertReferenceSymbol(symbol);
|
||||
assertDirectiveReference(symbol);
|
||||
});
|
||||
|
||||
function assertDirectiveReference(symbol: ReferenceSymbol) {
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('TestDir');
|
||||
expect((symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir');
|
||||
expect(symbol.declaration.name).toEqual('ref1');
|
||||
}
|
||||
|
||||
it('should get a symbol for local ref which refers to the template', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(templateNode.references[0], cmp)!;
|
||||
assertReferenceSymbol(symbol);
|
||||
assertTemplateReference(symbol);
|
||||
});
|
||||
|
||||
it('should get a symbol for usage local ref which refers to a template', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(
|
||||
(templateNode.children[0] as TmplAstTemplate).inputs[1].value, cmp)!;
|
||||
assertReferenceSymbol(symbol);
|
||||
assertTemplateReference(symbol);
|
||||
});
|
||||
|
||||
function assertTemplateReference(symbol: ReferenceSymbol) {
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('TemplateRef<any>');
|
||||
expect((symbol.target as TmplAstTemplate).tagName).toEqual('ng-template');
|
||||
expect(symbol.declaration.name).toEqual('ref0');
|
||||
}
|
||||
|
||||
it('should get symbol for the template itself', () => {
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(templateNode, cmp)!;
|
||||
assertTemplateSymbol(symbol);
|
||||
|
@ -150,7 +203,46 @@ runInEachFileSystem(() => {
|
|||
expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!))
|
||||
.toEqual('streetNumber');
|
||||
expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number');
|
||||
|
||||
const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!;
|
||||
expectUserSymbol(userSymbol);
|
||||
});
|
||||
|
||||
it('finds symbols for variables', () => {
|
||||
const userVar = templateNode.variables.find(v => v.name === 'user')!;
|
||||
const userSymbol = templateTypeChecker.getSymbolOfNode(userVar, cmp)!;
|
||||
expectUserSymbol(userSymbol);
|
||||
|
||||
const iVar = templateNode.variables.find(v => v.name === 'i')!;
|
||||
const iSymbol = templateTypeChecker.getSymbolOfNode(iVar, cmp)!;
|
||||
expectIndexSymbol(iSymbol);
|
||||
});
|
||||
|
||||
it('finds symbol when using a template variable', () => {
|
||||
const innerElementNodes =
|
||||
onlyAstElements((templateNode.children[0] as TmplAstElement).children);
|
||||
const indexSymbol =
|
||||
templateTypeChecker.getSymbolOfNode(innerElementNodes[0].inputs[0].value, cmp)!;
|
||||
expectIndexSymbol(indexSymbol);
|
||||
});
|
||||
|
||||
function expectUserSymbol(userSymbol: Symbol) {
|
||||
assertVariableSymbol(userSymbol);
|
||||
expect(userSymbol.tsSymbol!.escapedName).toContain('$implicit');
|
||||
expect(userSymbol.tsSymbol!.declarations[0].parent!.getText())
|
||||
.toContain('NgForOfContext');
|
||||
expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual('User');
|
||||
expect((userSymbol).declaration).toEqual(templateNode.variables[0]);
|
||||
}
|
||||
|
||||
function expectIndexSymbol(indexSymbol: Symbol) {
|
||||
assertVariableSymbol(indexSymbol);
|
||||
expect(indexSymbol.tsSymbol!.escapedName).toContain('index');
|
||||
expect(indexSymbol.tsSymbol!.declarations[0].parent!.getText())
|
||||
.toContain('NgForOfContext');
|
||||
expect(program.getTypeChecker().typeToString(indexSymbol.tsType!)).toEqual('number');
|
||||
expect((indexSymbol).declaration).toEqual(templateNode.variables[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -364,6 +456,89 @@ runInEachFileSystem(() => {
|
|||
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
||||
});
|
||||
|
||||
it('should get symbol for local reference of an Element', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {
|
||||
'Cmp': `
|
||||
<input #myRef>
|
||||
<div [input]="myRef"></div>`
|
||||
},
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const refSymbol = templateTypeChecker.getSymbolOfNode(nodes[0].references[0], cmp)!;
|
||||
assertReferenceSymbol(refSymbol);
|
||||
expect((refSymbol.target as TmplAstElement).name).toEqual('input');
|
||||
expect((refSymbol.declaration as TmplAstReference).name).toEqual('myRef');
|
||||
|
||||
const myRefUsage = templateTypeChecker.getSymbolOfNode(nodes[1].inputs[0].value, cmp)!;
|
||||
assertReferenceSymbol(myRefUsage);
|
||||
expect((myRefUsage.target as TmplAstElement).name).toEqual('input');
|
||||
expect((myRefUsage.declaration as TmplAstReference).name).toEqual('myRef');
|
||||
});
|
||||
|
||||
it('should get symbols for references which refer to directives', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const dirFile = absoluteFrom('/dir.ts');
|
||||
const templateString = `
|
||||
<div dir #myDir1="dir"></div>
|
||||
<div dir #myDir2="dir"></div>
|
||||
<div [inputA]="myDir1.dirValue" [inputB]="myDir1"></div>
|
||||
<div [inputA]="myDir2.dirValue" [inputB]="myDir2"></div>`;
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
declarations: [{
|
||||
name: 'TestDir',
|
||||
selector: '[dir]',
|
||||
file: dirFile,
|
||||
type: 'directive',
|
||||
exportAs: ['dir'],
|
||||
}]
|
||||
},
|
||||
{
|
||||
fileName: dirFile,
|
||||
source: `export class TestDir { dirValue = 'helloWorld' }`,
|
||||
templates: {}
|
||||
}
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const ref1Declaration = templateTypeChecker.getSymbolOfNode(nodes[0].references[0], cmp)!;
|
||||
assertReferenceSymbol(ref1Declaration);
|
||||
expect((ref1Declaration.target as ts.ClassDeclaration).name!.getText()).toEqual('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.declaration as TmplAstReference).name).toEqual('myDir2');
|
||||
|
||||
const dirValueSymbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[0].value, cmp)!;
|
||||
assertExpressionSymbol(dirValueSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(dirValueSymbol.tsSymbol!)).toBe('dirValue');
|
||||
expect(program.getTypeChecker().typeToString(dirValueSymbol.tsType)).toEqual('string');
|
||||
|
||||
const dir1Symbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[1].value, cmp)!;
|
||||
assertReferenceSymbol(dir1Symbol);
|
||||
expect((dir1Symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('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.declaration as TmplAstReference).name).toEqual('myDir2');
|
||||
});
|
||||
|
||||
describe('literals', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||
|
@ -1061,10 +1236,18 @@ function assertOutputBindingSymbol(tSymbol: Symbol): asserts tSymbol is OutputBi
|
|||
expect(tSymbol.kind).toEqual(SymbolKind.Output);
|
||||
}
|
||||
|
||||
function assertVariableSymbol(tSymbol: Symbol): asserts tSymbol is VariableSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Variable);
|
||||
}
|
||||
|
||||
function assertTemplateSymbol(tSymbol: Symbol): asserts tSymbol is TemplateSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Template);
|
||||
}
|
||||
|
||||
function assertReferenceSymbol(tSymbol: Symbol): asserts tSymbol is ReferenceSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Reference);
|
||||
}
|
||||
|
||||
function assertExpressionSymbol(tSymbol: Symbol): asserts tSymbol is ExpressionSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Expression);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue