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:
Andrew Scott 2020-08-27 13:51:05 -07:00 committed by Andrew Kushnir
parent f56ece4fdc
commit 19598b47ca
2 changed files with 252 additions and 5 deletions

View File

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

View File

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