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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user