diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index a6a123523f..1996140dca 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -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}); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index 52e77a3dcd..2daf890ba0 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -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'); + 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': ` + +
` + }, + }, + ]); + 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 = ` +
+
+
+
`; + 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; @@ -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); }