feat(compiler-cli): Add ability to get Symbol of AST expression in component template (#38618)
				
					
				
			Adds support to the `TemplateTypeChecker` to get a `Symbol` of an AST expression in a component template. Not all expressions will have `ts.Symbol`s (e.g. there is no `ts.Symbol` associated with the expression `a + b`, but there are for both the a and b nodes individually). PR Close #38618
This commit is contained in:
		
							parent
							
								
									cf2e8b99a8
								
							
						
					
					
						commit
						f56ece4fdc
					
				| @ -6,7 +6,7 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; | ||||
| import {AbsoluteSourceSpan, AST, ASTWithSource, BindingPipe, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {AbsoluteFsPath} from '../../file_system'; | ||||
| @ -40,7 +40,10 @@ export class SymbolBuilder { | ||||
|       return this.getSymbolOfElement(node); | ||||
|     } else if (node instanceof TmplAstTemplate) { | ||||
|       return this.getSymbolOfAstTemplate(node); | ||||
|     } else if (node instanceof AST) { | ||||
|       return this.getSymbolOfTemplateExpression(node); | ||||
|     } | ||||
|     // TODO(atscott): TmplAstContent, TmplAstIcu
 | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
| @ -223,6 +226,54 @@ export class SymbolBuilder { | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   private getSymbolOfTemplateExpression(expression: AST): ExpressionSymbol|null { | ||||
|     if (expression instanceof ASTWithSource) { | ||||
|       expression = expression.ast; | ||||
|     } | ||||
| 
 | ||||
|     let node = findFirstMatchingNode( | ||||
|         this.typeCheckBlock, | ||||
|         {withSpan: expression.sourceSpan, filter: (n: ts.Node): n is ts.Node => true}); | ||||
|     if (node === null) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     while (ts.isParenthesizedExpression(node)) { | ||||
|       node = node.expression; | ||||
|     } | ||||
| 
 | ||||
|     // - If we have safe property read ("a?.b") we want to get the Symbol for b, the `whenTrue`
 | ||||
|     // expression.
 | ||||
|     // - If our expression is a pipe binding ("a | test:b:c"), we want the Symbol for the
 | ||||
|     // `transform` on the pipe.
 | ||||
|     // - Otherwise, we retrieve the symbol for the node itself with no special considerations
 | ||||
|     if ((expression instanceof SafePropertyRead || expression instanceof SafeMethodCall) && | ||||
|         ts.isConditionalExpression(node)) { | ||||
|       const whenTrueSymbol = | ||||
|           (expression instanceof SafeMethodCall && ts.isCallExpression(node.whenTrue)) ? | ||||
|           this.getSymbolOfTsNode(node.whenTrue.expression) : | ||||
|           this.getSymbolOfTsNode(node.whenTrue); | ||||
|       if (whenTrueSymbol === null) { | ||||
|         return null; | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         ...whenTrueSymbol, | ||||
|         kind: SymbolKind.Expression, | ||||
|         // Rather than using the type of only the `whenTrue` part of the expression, we should
 | ||||
|         // still get the type of the whole conditional expression to include `|undefined`.
 | ||||
|         tsType: this.typeChecker.getTypeAtLocation(node) | ||||
|       }; | ||||
|     } else if (expression instanceof BindingPipe && ts.isCallExpression(node)) { | ||||
|       // TODO(atscott): Create a PipeSymbol to include symbol for the Pipe class
 | ||||
|       const symbolInfo = this.getSymbolOfTsNode(node.expression); | ||||
|       return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression}; | ||||
|     } else { | ||||
|       const symbolInfo = this.getSymbolOfTsNode(node); | ||||
|       return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression}; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo|null { | ||||
|     while (ts.isParenthesizedExpression(node)) { | ||||
|       node = node.expression; | ||||
|  | ||||
| @ -6,18 +6,38 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {TmplAstBoundAttribute, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; | ||||
| import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, 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, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig} from '../api'; | ||||
| import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig} from '../api'; | ||||
| 
 | ||||
| import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils'; | ||||
| 
 | ||||
| runInEachFileSystem(() => { | ||||
|   describe('TemplateTypeChecker.getSymbolOfNode', () => { | ||||
|     it('should not get a symbol for regular attributes', () => { | ||||
|       const fileName = absoluteFrom('/main.ts'); | ||||
|       const templateString = `<div id="helloWorld"></div>`; | ||||
|       const {templateTypeChecker, program} = setup( | ||||
|           [ | ||||
|             { | ||||
|               fileName, | ||||
|               templates: {'Cmp': templateString}, | ||||
|               source: `export class Cmp {}`, | ||||
|             }, | ||||
|           ], | ||||
|       ); | ||||
|       const sf = getSourceFileOrError(program, fileName); | ||||
|       const cmp = getClass(sf, 'Cmp'); | ||||
|       const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; | ||||
| 
 | ||||
|       const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp); | ||||
|       expect(symbol).toBeNull(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('templates', () => { | ||||
|       describe('ng-templates', () => { | ||||
|         let templateTypeChecker: TemplateTypeChecker; | ||||
| @ -67,6 +87,394 @@ runInEachFileSystem(() => { | ||||
|           expect(symbol.directives[0].tsSymbol.getName()).toBe('TestDir'); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('structural directives', () => { | ||||
|         let templateTypeChecker: TemplateTypeChecker; | ||||
|         let cmp: ClassDeclaration<ts.ClassDeclaration>; | ||||
|         let templateNode: TmplAstTemplate; | ||||
|         let program: ts.Program; | ||||
| 
 | ||||
|         beforeEach(() => { | ||||
|           const fileName = absoluteFrom('/main.ts'); | ||||
|           const templateString = ` | ||||
|               <div *ngFor="let user of users; let i = index;"> | ||||
|                 {{user.name}} {{user.streetNumber}} | ||||
|                 <div [tabIndex]="i"></div> | ||||
|               </div>`;
 | ||||
|           const testValues = setup([ | ||||
|             { | ||||
|               fileName, | ||||
|               templates: {'Cmp': templateString}, | ||||
|               source: ` | ||||
|             export interface User { | ||||
|               name: string; | ||||
|               streetNumber: number; | ||||
|             } | ||||
|             export class Cmp { users: User[]; } | ||||
|             `,
 | ||||
|               declarations: [ngForDeclaration()], | ||||
|             }, | ||||
|             ngForTypeCheckTarget(), | ||||
|           ]); | ||||
|           templateTypeChecker = testValues.templateTypeChecker; | ||||
|           program = testValues.program; | ||||
|           const sf = getSourceFileOrError(testValues.program, fileName); | ||||
|           cmp = getClass(sf, 'Cmp'); | ||||
|           templateNode = getAstTemplates(templateTypeChecker, cmp)[0]; | ||||
|         }); | ||||
| 
 | ||||
|         it('should retrieve a symbol for an expression inside structural binding', () => { | ||||
|           const ngForOfBinding = | ||||
|               templateNode.templateAttrs.find(a => a.name === 'ngForOf')! as TmplAstBoundAttribute; | ||||
|           const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding.value, cmp)!; | ||||
|           assertExpressionSymbol(symbol); | ||||
|           expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('users'); | ||||
|           expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array<User>'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should retrieve a symbol for property reads of implicit variable inside structural binding', | ||||
|            () => { | ||||
|              const boundText = | ||||
|                  (templateNode.children[0] as TmplAstElement).children[0] as TmplAstBoundText; | ||||
|              const interpolation = (boundText.value as ASTWithSource).ast as Interpolation; | ||||
|              const namePropRead = interpolation.expressions[0] as PropertyRead; | ||||
|              const streetNumberPropRead = interpolation.expressions[1] as PropertyRead; | ||||
| 
 | ||||
|              const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!; | ||||
|              assertExpressionSymbol(nameSymbol); | ||||
|              expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name'); | ||||
|              expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string'); | ||||
| 
 | ||||
|              const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; | ||||
|              assertExpressionSymbol(streetSymbol); | ||||
|              expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)) | ||||
|                  .toEqual('streetNumber'); | ||||
|              expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); | ||||
|            }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for expressions', () => { | ||||
|       it('should get a symbol for a component property used in an input binding', () => { | ||||
|         const fileName = absoluteFrom('/main.ts'); | ||||
|         const templateString = `<div [inputA]="helloWorld"></div>`; | ||||
|         const {templateTypeChecker, program} = setup([ | ||||
|           { | ||||
|             fileName, | ||||
|             templates: {'Cmp': templateString}, | ||||
|             source: `export class Cmp {helloWorld?: boolean;}`, | ||||
|           }, | ||||
|         ]); | ||||
|         const sf = getSourceFileOrError(program, fileName); | ||||
|         const cmp = getClass(sf, 'Cmp'); | ||||
|         const nodes = getAstElements(templateTypeChecker, cmp); | ||||
| 
 | ||||
|         const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!; | ||||
|         assertExpressionSymbol(symbol); | ||||
|         expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld'); | ||||
|         expect(program.getTypeChecker().typeToString(symbol.tsType)) | ||||
|             .toEqual('false | true | undefined'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should get a symbol for properties several levels deep', () => { | ||||
|         const fileName = absoluteFrom('/main.ts'); | ||||
|         const templateString = `<div [inputA]="person.address.street"></div>`; | ||||
|         const {templateTypeChecker, program} = setup([ | ||||
|           { | ||||
|             fileName, | ||||
|             templates: {'Cmp': templateString}, | ||||
|             source: ` | ||||
|               interface Address { | ||||
|                 street: string; | ||||
|               } | ||||
| 
 | ||||
|               interface Person { | ||||
|                 address: Address; | ||||
|               } | ||||
|               export class Cmp {person?: Person;} | ||||
|             `,
 | ||||
|           }, | ||||
|         ]); | ||||
|         const sf = getSourceFileOrError(program, fileName); | ||||
|         const cmp = getClass(sf, 'Cmp'); | ||||
|         const nodes = getAstElements(templateTypeChecker, cmp); | ||||
| 
 | ||||
|         const inputNode = nodes[0].inputs[0].value as ASTWithSource; | ||||
| 
 | ||||
|         const symbol = templateTypeChecker.getSymbolOfNode(inputNode, cmp)!; | ||||
|         assertExpressionSymbol(symbol); | ||||
|         expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('street'); | ||||
|         expect((symbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration).parent.name!.getText()) | ||||
|             .toEqual('Address'); | ||||
|         expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('string'); | ||||
| 
 | ||||
|         const personSymbol = templateTypeChecker.getSymbolOfNode( | ||||
|             ((inputNode.ast as PropertyRead).receiver as PropertyRead).receiver, cmp)!; | ||||
|         assertExpressionSymbol(personSymbol); | ||||
|         expect(program.getTypeChecker().symbolToString(personSymbol.tsSymbol!)).toEqual('person'); | ||||
|         expect(program.getTypeChecker().typeToString(personSymbol.tsType)) | ||||
|             .toEqual('Person | undefined'); | ||||
|       }); | ||||
| 
 | ||||
|       describe('should get symbols for conditionals', () => { | ||||
|         let templateTypeChecker: TemplateTypeChecker; | ||||
|         let cmp: ClassDeclaration<ts.ClassDeclaration>; | ||||
|         let program: ts.Program; | ||||
|         let templateString: string; | ||||
| 
 | ||||
|         beforeEach(() => { | ||||
|           const fileName = absoluteFrom('/main.ts'); | ||||
|           templateString = ` | ||||
|         <div [inputA]="person?.address?.street"></div> | ||||
|         <div [inputA]="person ? person.address : noPersonError"></div> | ||||
|         <div [inputA]="person?.speak()"></div> | ||||
|       `;
 | ||||
|           const testValues = setup( | ||||
|               [ | ||||
|                 { | ||||
|                   fileName, | ||||
|                   templates: {'Cmp': templateString}, | ||||
|                   source: ` | ||||
|               interface Address { | ||||
|                 street: string; | ||||
|               } | ||||
| 
 | ||||
|               interface Person { | ||||
|                 address: Address; | ||||
|                 speak(): string; | ||||
|               } | ||||
|               export class Cmp {person?: Person; noPersonError = 'no person'} | ||||
|             `,
 | ||||
|                 }, | ||||
|               ], | ||||
|           ); | ||||
|           templateTypeChecker = testValues.templateTypeChecker; | ||||
|           program = testValues.program; | ||||
|           const sf = getSourceFileOrError(program, fileName); | ||||
|           cmp = getClass(sf, 'Cmp'); | ||||
|         }); | ||||
| 
 | ||||
|         it('safe property reads', () => { | ||||
|           const nodes = getAstElements(templateTypeChecker, cmp); | ||||
|           const safePropertyRead = nodes[0].inputs[0].value as ASTWithSource; | ||||
|           const propReadSymbol = templateTypeChecker.getSymbolOfNode(safePropertyRead, cmp)!; | ||||
|           assertExpressionSymbol(propReadSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(propReadSymbol.tsSymbol!)) | ||||
|               .toEqual('street'); | ||||
|           expect((propReadSymbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration) | ||||
|                      .parent.name!.getText()) | ||||
|               .toEqual('Address'); | ||||
|           expect(program.getTypeChecker().typeToString(propReadSymbol.tsType)) | ||||
|               .toEqual('string | undefined'); | ||||
|         }); | ||||
| 
 | ||||
|         it('safe method calls', () => { | ||||
|           const nodes = getAstElements(templateTypeChecker, cmp); | ||||
|           const safeMethodCall = nodes[2].inputs[0].value as ASTWithSource; | ||||
|           const methodCallSymbol = templateTypeChecker.getSymbolOfNode(safeMethodCall, cmp)!; | ||||
|           assertExpressionSymbol(methodCallSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(methodCallSymbol.tsSymbol!)) | ||||
|               .toEqual('speak'); | ||||
|           expect((methodCallSymbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration) | ||||
|                      .parent.name!.getText()) | ||||
|               .toEqual('Person'); | ||||
|           expect(program.getTypeChecker().typeToString(methodCallSymbol.tsType)) | ||||
|               .toEqual('string | undefined'); | ||||
|         }); | ||||
| 
 | ||||
|         it('ternary expressions', () => { | ||||
|           const nodes = getAstElements(templateTypeChecker, cmp); | ||||
| 
 | ||||
|           const ternary = (nodes[1].inputs[0].value as ASTWithSource).ast as Conditional; | ||||
|           const ternarySymbol = templateTypeChecker.getSymbolOfNode(ternary, cmp)!; | ||||
|           assertExpressionSymbol(ternarySymbol); | ||||
|           expect(ternarySymbol.tsSymbol).toBeNull(); | ||||
|           expect(program.getTypeChecker().typeToString(ternarySymbol.tsType)) | ||||
|               .toEqual('string | Address'); | ||||
|           const addrSymbol = templateTypeChecker.getSymbolOfNode(ternary.trueExp, cmp)!; | ||||
|           assertExpressionSymbol(addrSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(addrSymbol.tsSymbol!)).toEqual('address'); | ||||
|           expect(program.getTypeChecker().typeToString(addrSymbol.tsType)).toEqual('Address'); | ||||
| 
 | ||||
|           const noPersonSymbol = templateTypeChecker.getSymbolOfNode(ternary.falseExp, cmp)!; | ||||
|           assertExpressionSymbol(noPersonSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(noPersonSymbol.tsSymbol!)) | ||||
|               .toEqual('noPersonError'); | ||||
|           expect(program.getTypeChecker().typeToString(noPersonSymbol.tsType)).toEqual('string'); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should get a symbol for function on a component used in an input binding', () => { | ||||
|         const fileName = absoluteFrom('/main.ts'); | ||||
|         const templateString = `<div [inputA]="helloWorld"></div>`; | ||||
|         const {templateTypeChecker, program} = setup([ | ||||
|           { | ||||
|             fileName, | ||||
|             templates: {'Cmp': templateString}, | ||||
|             source: ` | ||||
|             export class Cmp { | ||||
|               helloWorld() { return ''; } | ||||
|             }`,
 | ||||
|           }, | ||||
|         ]); | ||||
|         const sf = getSourceFileOrError(program, fileName); | ||||
|         const cmp = getClass(sf, 'Cmp'); | ||||
|         const nodes = getAstElements(templateTypeChecker, cmp); | ||||
| 
 | ||||
|         const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!; | ||||
|         assertExpressionSymbol(symbol); | ||||
|         expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld'); | ||||
|         expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('() => string'); | ||||
|       }); | ||||
| 
 | ||||
|       it('should get a symbol for binary expressions', () => { | ||||
|         const fileName = absoluteFrom('/main.ts'); | ||||
|         const templateString = `<div [inputA]="a + b"></div>`; | ||||
|         const {templateTypeChecker, program} = setup([ | ||||
|           { | ||||
|             fileName, | ||||
|             templates: {'Cmp': templateString}, | ||||
|             source: ` | ||||
|             export class Cmp { | ||||
|               a!: string; | ||||
|               b!: number; | ||||
|             }`,
 | ||||
|           }, | ||||
|         ]); | ||||
|         const sf = getSourceFileOrError(program, fileName); | ||||
|         const cmp = getClass(sf, 'Cmp'); | ||||
|         const nodes = getAstElements(templateTypeChecker, cmp); | ||||
| 
 | ||||
|         const valueAssignment = nodes[0].inputs[0].value as ASTWithSource; | ||||
|         const wholeExprSymbol = templateTypeChecker.getSymbolOfNode(valueAssignment, cmp)!; | ||||
|         assertExpressionSymbol(wholeExprSymbol); | ||||
|         expect(wholeExprSymbol.tsSymbol).toBeNull(); | ||||
|         expect(program.getTypeChecker().typeToString(wholeExprSymbol.tsType)).toEqual('string'); | ||||
| 
 | ||||
|         const aSymbol = | ||||
|             templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).left, cmp)!; | ||||
|         assertExpressionSymbol(aSymbol); | ||||
|         expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toBe('a'); | ||||
|         expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); | ||||
| 
 | ||||
|         const bSymbol = | ||||
|             templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).right, cmp)!; | ||||
|         assertExpressionSymbol(bSymbol); | ||||
|         expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toBe('b'); | ||||
|         expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); | ||||
|       }); | ||||
| 
 | ||||
|       describe('literals', () => { | ||||
|         let templateTypeChecker: TemplateTypeChecker; | ||||
|         let cmp: ClassDeclaration<ts.ClassDeclaration>; | ||||
|         let interpolation: Interpolation; | ||||
|         let program: ts.Program; | ||||
| 
 | ||||
|         beforeEach(() => { | ||||
|           const fileName = absoluteFrom('/main.ts'); | ||||
|           const templateString = ` | ||||
|           {{ [1, 2, 3] }} | ||||
|           {{ { hello: "world" } }}`;
 | ||||
|           const testValues = setup([ | ||||
|             { | ||||
|               fileName, | ||||
|               templates: {'Cmp': templateString}, | ||||
|               source: `export class Cmp {}`, | ||||
|             }, | ||||
|           ]); | ||||
|           templateTypeChecker = testValues.templateTypeChecker; | ||||
|           program = testValues.program; | ||||
|           const sf = getSourceFileOrError(testValues.program, fileName); | ||||
|           cmp = getClass(sf, 'Cmp'); | ||||
|           interpolation = ((templateTypeChecker.getTemplate(cmp)![0] as TmplAstBoundText).value as | ||||
|                            ASTWithSource) | ||||
|                               .ast as Interpolation; | ||||
|         }); | ||||
| 
 | ||||
|         it('literal array', () => { | ||||
|           const literalArray = interpolation.expressions[0]; | ||||
|           const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!; | ||||
|           assertExpressionSymbol(symbol); | ||||
|           expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array'); | ||||
|           expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array<number>'); | ||||
|         }); | ||||
| 
 | ||||
|         it('literal map', () => { | ||||
|           const literalMap = interpolation.expressions[1]; | ||||
|           const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!; | ||||
|           assertExpressionSymbol(symbol); | ||||
|           expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object'); | ||||
|           expect(program.getTypeChecker().typeToString(symbol.tsType)) | ||||
|               .toEqual('{ hello: string; }'); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
| 
 | ||||
|       describe('pipes', () => { | ||||
|         let templateTypeChecker: TemplateTypeChecker; | ||||
|         let cmp: ClassDeclaration<ts.ClassDeclaration>; | ||||
|         let binding: BindingPipe; | ||||
|         let program: ts.Program; | ||||
| 
 | ||||
|         beforeEach(() => { | ||||
|           const fileName = absoluteFrom('/main.ts'); | ||||
|           const templateString = `<div [inputA]="a | test:b:c"></div>`; | ||||
|           const testValues = setup([ | ||||
|             { | ||||
|               fileName, | ||||
|               templates: {'Cmp': templateString}, | ||||
|               source: ` | ||||
|             export class Cmp { a: string; b: number; c: boolean } | ||||
|             export class TestPipe { | ||||
|               transform(value: string, repeat: number, commaSeparate: boolean): string[] { | ||||
|               } | ||||
|             } | ||||
|             `,
 | ||||
|               declarations: [{ | ||||
|                 type: 'pipe', | ||||
|                 name: 'TestPipe', | ||||
|                 pipeName: 'test', | ||||
|               }], | ||||
|             }, | ||||
|           ]); | ||||
|           program = testValues.program; | ||||
|           templateTypeChecker = testValues.templateTypeChecker; | ||||
|           const sf = getSourceFileOrError(testValues.program, fileName); | ||||
|           cmp = getClass(sf, 'Cmp'); | ||||
|           binding = | ||||
|               (getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource).ast as | ||||
|               BindingPipe; | ||||
|         }); | ||||
| 
 | ||||
|         it('should get symbol for pipe', () => { | ||||
|           const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!; | ||||
|           assertExpressionSymbol(pipeSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!)) | ||||
|               .toEqual('transform'); | ||||
|           expect( | ||||
|               (pipeSymbol.tsSymbol!.declarations[0].parent as ts.ClassDeclaration).name!.getText()) | ||||
|               .toEqual('TestPipe'); | ||||
|           expect(program.getTypeChecker().typeToString(pipeSymbol.tsType)) | ||||
|               .toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should get symbols for pipe expression and args', () => { | ||||
|           const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!; | ||||
|           assertExpressionSymbol(aSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a'); | ||||
|           expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); | ||||
| 
 | ||||
|           const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0], cmp)!; | ||||
|           assertExpressionSymbol(bSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b'); | ||||
|           expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); | ||||
| 
 | ||||
|           const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1], cmp)!; | ||||
|           assertExpressionSymbol(cSymbol); | ||||
|           expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c'); | ||||
|           expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('input bindings', () => { | ||||
| @ -627,6 +1035,15 @@ function onlyAstTemplates(nodes: TmplAstNode[]): TmplAstTemplate[] { | ||||
|   return nodes.filter((n): n is TmplAstTemplate => n instanceof TmplAstTemplate); | ||||
| } | ||||
| 
 | ||||
| function onlyAstElements(nodes: TmplAstNode[]): TmplAstElement[] { | ||||
|   return nodes.filter((n): n is TmplAstElement => n instanceof TmplAstElement); | ||||
| } | ||||
| 
 | ||||
| function getAstElements( | ||||
|     templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) { | ||||
|   return onlyAstElements(templateTypeChecker.getTemplate(cmp)!); | ||||
| } | ||||
| 
 | ||||
| function getAstTemplates( | ||||
|     templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) { | ||||
|   return onlyAstTemplates(templateTypeChecker.getTemplate(cmp)!); | ||||
| @ -648,6 +1065,10 @@ function assertTemplateSymbol(tSymbol: Symbol): asserts tSymbol is TemplateSymbo | ||||
|   expect(tSymbol.kind).toEqual(SymbolKind.Template); | ||||
| } | ||||
| 
 | ||||
| function assertExpressionSymbol(tSymbol: Symbol): asserts tSymbol is ExpressionSymbol { | ||||
|   expect(tSymbol.kind).toEqual(SymbolKind.Expression); | ||||
| } | ||||
| 
 | ||||
| function assertElementSymbol(tSymbol: Symbol): asserts tSymbol is ElementSymbol { | ||||
|   expect(tSymbol.kind).toEqual(SymbolKind.Element); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user