diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts index 1eed4de010..630980dc0d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -104,6 +104,7 @@ runInEachFileSystem(() => { outputs: analysis.outputs, isComponent: false, name: 'Dir', + selector: '[dir]', }; matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts index 062d67b04d..fd4fb74749 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -21,13 +21,14 @@ export enum SymbolKind { Element, Template, Expression, + DomBinding, } /** * A representation of an entity in the `TemplateAst`. */ export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol| - VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol; + VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol|DomBindingSymbol; /** Information about where a `ts.Node` can be found in the type check block shim file. */ export interface ShimLocation { @@ -227,4 +228,22 @@ export interface DirectiveSymbol { /** The location in the shim file for the variable that holds the type of the directive. */ shimLocation: ShimLocation; + + /** The selector for the `Directive` / `Component`. */ + selector: string|null; + + /** `true` if this `DirectiveSymbol` is for a @Component. */ + isComponent: boolean; +} + +/** + * A representation of an attribute on an element or template. These bindings aren't currently + * type-checked (see `checkTypeOfDomBindings`) so they won't have a `ts.Type`, `ts.Symbol`, or shim + * location. + */ +export interface DomBindingSymbol { + kind: SymbolKind.DomBinding; + + /** The symbol for the element or template of the text attribute. */ + host: ElementSymbol|TemplateSymbol; } 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 1996140dca..6ce9b6ee8e 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,18 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteSourceSpan, AST, ASTWithSource, BindingPipe, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, 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, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, VariableSymbol} from '../api'; +import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api'; import {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments'; import {TemplateData} from './context'; +import {isAccessExpression} from './ts_util'; import {TcbDirectiveOutputsOp} from './type_check_block'; - /** * A class which extracts information from a type check block. * This class is essentially used as just a closure around the constructor parameters. @@ -31,7 +31,7 @@ export class SymbolBuilder { getSymbol(node: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null; getSymbol(node: AST|TmplAstNode): Symbol|null; getSymbol(node: AST|TmplAstNode): Symbol|null { - if (node instanceof TmplAstBoundAttribute) { + if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute) { // TODO(atscott): input and output bindings only return the first directive match but should // return a list of bindings for all of them. return this.getSymbolOfInputBinding(node); @@ -85,24 +85,55 @@ export class SymbolBuilder { private getDirectivesOfNode(element: TmplAstElement|TmplAstTemplate): DirectiveSymbol[] { const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan; const tcbSourceFile = this.typeCheckBlock.getSourceFile(); - const isDirectiveDeclaration = (node: ts.Node): node is ts.TypeNode => ts.isTypeNode(node) && + // directives could be either: + // - var _t1: TestDir /*T:D*/ = (null!); + // - var _t1 /*T:D*/ = _ctor1({}); + const isDirectiveDeclaration = (node: ts.Node): node is ts.TypeNode|ts.Identifier => + (ts.isTypeNode(node) || ts.isIdentifier(node)) && hasExpressionIdentifier(tcbSourceFile, node, ExpressionIdentifier.DIRECTIVE); const nodes = findAllMatchingNodes( this.typeCheckBlock, {withSpan: elementSourceSpan, filter: isDirectiveDeclaration}); return nodes .map(node => { - const symbol = this.getSymbolOfTsNode(node); - if (symbol === null || symbol.tsSymbol === null) { + const symbol = (ts.isIdentifier(node) && ts.isVariableDeclaration(node.parent)) ? + this.getSymbolOfVariableDeclaration(node.parent) : + this.getSymbolOfTsNode(node); + if (symbol === null || symbol.tsSymbol === null || + symbol.tsSymbol.declarations.length === 0) { return null; } - const directiveSymbol: - DirectiveSymbol = {...symbol, tsSymbol: symbol.tsSymbol, kind: SymbolKind.Directive}; + + const meta = this.getDirectiveMeta(element, symbol.tsSymbol.declarations[0]); + if (meta === null) { + return null; + } + + const selector = meta.selector ?? null; + const isComponent = meta.isComponent ?? null; + const directiveSymbol: DirectiveSymbol = { + ...symbol, + tsSymbol: symbol.tsSymbol, + selector, + isComponent, + kind: SymbolKind.Directive + }; return directiveSymbol; }) .filter((d): d is DirectiveSymbol => d !== null); } + private getDirectiveMeta( + host: TmplAstTemplate|TmplAstElement, + directiveDeclaration: ts.Declaration): TypeCheckableDirectiveMeta|null { + const directives = this.templateData.boundTarget.getDirectivesOfNode(host); + if (directives === null) { + return null; + } + + return directives.find(m => m.ref.node === directiveDeclaration) ?? null; + } + private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null { // Outputs are a `ts.CallExpression` that look like one of the two: // * _outputHelper(_t1["outputField"]).subscribe(handler); @@ -114,7 +145,8 @@ export class SymbolBuilder { } const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding); - if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) { + if (consumer === null || consumer instanceof TmplAstTemplate || + consumer instanceof TmplAstElement) { // Bindings to element or template events produce `addEventListener` which // we cannot get the field for. return null; @@ -130,12 +162,12 @@ export class SymbolBuilder { } - const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess); + const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer); if (target === null) { return null; } - const positionInShimFile = outputFieldAccess.argumentExpression.getStart(); + const positionInShimFile = this.getShimPositionForNode(outputFieldAccess); const tsType = this.typeChecker.getTypeAtLocation(node); return { kind: SymbolKind.Output, @@ -149,40 +181,30 @@ export class SymbolBuilder { }; } - private getSymbolOfInputBinding(attributeBinding: TmplAstBoundAttribute): InputBindingSymbol - |null { + private getSymbolOfInputBinding(binding: TmplAstBoundAttribute| + TmplAstTextAttribute): InputBindingSymbol|DomBindingSymbol|null { + const consumer = this.templateData.boundTarget.getConsumerOfBinding(binding); + if (consumer === null) { + return null; + } + + if (consumer instanceof TmplAstElement || consumer instanceof TmplAstTemplate) { + const host = this.getSymbol(consumer); + return host !== null ? {kind: SymbolKind.DomBinding, host} : null; + } + const node = findFirstMatchingNode( - this.typeCheckBlock, {withSpan: attributeBinding.sourceSpan, filter: isAssignment}); - if (node === null) { + this.typeCheckBlock, {withSpan: binding.sourceSpan, filter: isAssignment}); + if (node === null || !isAccessExpression(node.left)) { return null; } - let tsSymbol: ts.Symbol|undefined; - let positionInShimFile: number|null = null; - let tsType: ts.Type; - if (ts.isElementAccessExpression(node.left)) { - tsSymbol = this.typeChecker.getSymbolAtLocation(node.left.argumentExpression); - positionInShimFile = node.left.argumentExpression.getStart(); - tsType = this.typeChecker.getTypeAtLocation(node.left.argumentExpression); - } else if (ts.isPropertyAccessExpression(node.left)) { - tsSymbol = this.typeChecker.getSymbolAtLocation(node.left.name); - positionInShimFile = node.left.name.getStart(); - tsType = this.typeChecker.getTypeAtLocation(node.left.name); - } else { - return null; - } - if (tsSymbol === undefined || positionInShimFile === null) { + const symbolInfo = this.getSymbolOfTsNode(node.left); + if (symbolInfo === null || symbolInfo.tsSymbol === null) { return null; } - const consumer = this.templateData.boundTarget.getConsumerOfBinding(attributeBinding); - let target: ElementSymbol|TemplateSymbol|DirectiveSymbol|null; - if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) { - target = this.getSymbol(consumer); - } else { - target = this.getDirectiveSymbolForAccessExpression(node.left); - } - + const target = this.getDirectiveSymbolForAccessExpression(node.left, consumer); if (target === null) { return null; } @@ -190,17 +212,17 @@ export class SymbolBuilder { return { kind: SymbolKind.Input, bindings: [{ + ...symbolInfo, + tsSymbol: symbolInfo.tsSymbol, kind: SymbolKind.Binding, - tsSymbol, - tsType, target, - shimLocation: {shimPath: this.shimPath, positionInShimFile}, }], }; } - private getDirectiveSymbolForAccessExpression(node: ts.ElementAccessExpression| - ts.PropertyAccessExpression): DirectiveSymbol|null { + private getDirectiveSymbolForAccessExpression( + node: ts.ElementAccessExpression|ts.PropertyAccessExpression, + {isComponent, selector}: TypeCheckableDirectiveMeta): DirectiveSymbol|null { // In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1. // The retrieved symbol for _t1 will be the variable declaration. const tsSymbol = this.typeChecker.getSymbolAtLocation(node.expression); @@ -228,6 +250,8 @@ export class SymbolBuilder { tsSymbol: symbol.tsSymbol, tsType: symbol.tsType, shimLocation: symbol.shimLocation, + isComponent, + selector, }; } @@ -295,9 +319,14 @@ export class SymbolBuilder { return this.getSymbol(expressionTarget); } + // The `name` part of a `PropertyWrite` and `MethodCall` does not have its own + // AST so there is no way to retrieve a `Symbol` for just the `name` via a specific node. + const withSpan = (expression instanceof PropertyWrite || expression instanceof MethodCall) ? + expression.nameSpan : + expression.sourceSpan; + let node = findFirstMatchingNode( - this.typeCheckBlock, - {withSpan: expression.sourceSpan, filter: (n: ts.Node): n is ts.Node => true}); + this.typeCheckBlock, {withSpan, filter: (n: ts.Node): n is ts.Node => true}); if (node === null) { return null; } @@ -344,20 +373,20 @@ export class SymbolBuilder { } let tsSymbol: ts.Symbol|undefined; - let positionInShimFile: number; if (ts.isPropertyAccessExpression(node)) { tsSymbol = this.typeChecker.getSymbolAtLocation(node.name); - positionInShimFile = node.name.getStart(); + } else if (ts.isElementAccessExpression(node)) { + tsSymbol = this.typeChecker.getSymbolAtLocation(node.argumentExpression); } else { tsSymbol = this.typeChecker.getSymbolAtLocation(node); - positionInShimFile = node.getStart(); } + const positionInShimFile = this.getShimPositionForNode(node); const type = this.typeChecker.getTypeAtLocation(node); return { // If we could not find a symbol, fall back to the symbol on the type for the node. // Some nodes won't have a "symbol at location" but will have a symbol for the type. - // One example of this would be literals. + // Examples of this would be literals and `document.createElement('div')`. tsSymbol: tsSymbol ?? type.symbol ?? null, tsType: type, shimLocation: {shimPath: this.shimPath, positionInShimFile}, @@ -381,7 +410,20 @@ export class SymbolBuilder { if (symbol === null) { return null; } - return symbol; } + + private getShimPositionForNode(node: ts.Node): number { + if (ts.isTypeReferenceNode(node)) { + return this.getShimPositionForNode(node.typeName); + } else if (ts.isQualifiedName(node)) { + return node.right.getStart(); + } else if (ts.isPropertyAccessExpression(node)) { + return node.name.getStart(); + } else if (ts.isElementAccessExpression(node)) { + return node.argumentExpression.getStart(); + } else { + return node.getStart(); + } + } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts index 96ff8178a4..8356fbd1f1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts @@ -172,3 +172,8 @@ export function checkIfGenericTypesAreUnbound(node: ClassDeclaration param.constraint === undefined); } + +export function isAccessExpression(node: ts.Node): node is ts.ElementAccessExpression| + ts.PropertyAccessExpression { + return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node); +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index b756e306c9..217dd6024c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -433,6 +433,7 @@ function prepareDeclarations( name: decl.name, ref: new Reference(resolveDeclaration(decl)), exportAs: decl.exportAs || null, + selector: decl.selector || null, hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard || false, inputs: ClassPropertyMapping.fromMappedObject(decl.inputs || {}), isComponent: decl.isComponent || false, 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 0b36ec4b5c..d1b5d3fb7f 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 @@ -12,13 +12,13 @@ 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, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api'; +import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} 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', () => { + it('should get a symbol for regular attributes', () => { const fileName = absoluteFrom('/main.ts'); const templateString = `
`; const {templateTypeChecker, program} = setup( @@ -34,8 +34,44 @@ runInEachFileSystem(() => { const cmp = getClass(sf, 'Cmp'); const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; - const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp); - expect(symbol).toBeNull(); + const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; + assertDomBindingSymbol(symbol); + assertElementSymbol(symbol.host); + }); + + it('should get a symbol for text attributes corresponding with a directive input', () => { + const fileName = absoluteFrom('/main.ts'); + const dirFile = absoluteFrom('/dir.ts'); + const templateString = `
`; + const {templateTypeChecker, program} = setup( + [ + { + fileName, + templates: {'Cmp': templateString}, + declarations: [{ + name: 'NameDiv', + selector: 'div[name]', + file: dirFile, + type: 'directive', + inputs: {name: 'name'}, + }] + }, + { + fileName: dirFile, + source: `export class NameDiv {name!: string;}`, + templates: {}, + } + ], + ); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; + + const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; + assertInputBindingSymbol(symbol); + expect( + (symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText()) + .toEqual('name'); }); describe('templates', () => { @@ -650,6 +686,54 @@ runInEachFileSystem(() => { expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); }); }); + + + it('should get a symbol for PropertyWrite expressions', () => { + const fileName = absoluteFrom('/main.ts'); + const {templateTypeChecker, program} = setup([ + { + fileName, + templates: {'Cmp': '
'}, + source: `export class Cmp { lastEvent: any; }` + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + const node = getAstElements(templateTypeChecker, cmp)[0]; + const writeSymbol = templateTypeChecker.getSymbolOfNode(node.outputs[0].handler, cmp)!; + assertExpressionSymbol(writeSymbol); + // Note that the symbol returned is for the RHS of the PropertyWrite. The AST + // does not support specific designation for the RHS so we assume that's what + // is wanted in this case. We don't support retrieving a symbol for the whole + // expression and if you want to get a symbol for the '$event', you can + // use the `value` AST of the `PropertyWrite`. + expect(program.getTypeChecker().symbolToString(writeSymbol.tsSymbol!)).toEqual('lastEvent'); + expect(program.getTypeChecker().typeToString(writeSymbol.tsType)).toEqual('any'); + }); + + it('should get a symbol for MethodCall expressions', () => { + const fileName = absoluteFrom('/main.ts'); + const {templateTypeChecker, program} = setup([ + { + fileName, + templates: {'Cmp': '
'}, + source: `export class Cmp { toString(v: any): string { return String(v); } }` + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + const node = getAstElements(templateTypeChecker, cmp)[0]; + const callSymbol = templateTypeChecker.getSymbolOfNode(node.inputs[0].value, cmp)!; + assertExpressionSymbol(callSymbol); + // Note that the symbol returned is for the method name of the MethodCall. The AST + // does not support specific designation for the name so we assume that's what + // is wanted in this case. We don't support retrieving a symbol for the whole + // call expression and if you want to get a symbol for the args, you can + // use the AST of the args in the `MethodCall`. + expect(program.getTypeChecker().symbolToString(callSymbol.tsSymbol!)).toEqual('toString'); + expect(program.getTypeChecker().typeToString(callSymbol.tsType)) + .toEqual('(v: any) => string'); + }); }); describe('input bindings', () => { @@ -750,9 +834,9 @@ runInEachFileSystem(() => { .toEqual('ngForOf'); }); - it('returns empty list when there is no directive registered for the binding', () => { + it('returns dom binding input binds only to the dom element', () => { const fileName = absoluteFrom('/main.ts'); - const templateString = `
`; + const templateString = `
`; const {program, templateTypeChecker} = setup([ {fileName, templates: {'Cmp': templateString}, declarations: []}, ]); @@ -762,11 +846,12 @@ runInEachFileSystem(() => { const nodes = templateTypeChecker.getTemplate(cmp)!; const binding = (nodes[0] as TmplAstElement).inputs[0]; - const symbol = templateTypeChecker.getSymbolOfNode(binding, cmp); - expect(symbol).toBeNull(); + const symbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!; + assertDomBindingSymbol(symbol); + assertElementSymbol(symbol.host); }); - it('returns empty list when directive members do not match the input', () => { + it('returns dom binding when directive members do not match the input', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); const templateString = `
`; @@ -794,8 +879,9 @@ runInEachFileSystem(() => { const nodes = templateTypeChecker.getTemplate(cmp)!; const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; - const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp); - expect(symbol).toBeNull(); + const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; + assertDomBindingSymbol(symbol); + assertElementSymbol(symbol.host); }); it('can match binding when there are two directives', () => { @@ -1120,6 +1206,7 @@ runInEachFileSystem(() => { { name: 'ChildComponent', selector: 'child-component', + isComponent: true, file: dirFile, type: 'directive', }, @@ -1145,6 +1232,7 @@ runInEachFileSystem(() => { assertDirectiveSymbol(symbol.directives[0]); expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType)) .toEqual('ChildComponent'); + expect(symbol.directives[0].isComponent).toBe(true); }); it('element with directive matches', () => { @@ -1199,8 +1287,52 @@ runInEachFileSystem(() => { const actualDirectives = symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort(); expect(actualDirectives).toEqual(expectedDirectives); + + const expectedSelectors = ['[dir]', '[dir2]', 'div'].sort(); + const actualSelectors = symbol.directives.map(dir => dir.selector).sort(); + expect(actualSelectors).toEqual(expectedSelectors); }); }); + + it('elements with generic directives', () => { + const fileName = absoluteFrom('/main.ts'); + const dirFile = absoluteFrom('/dir.ts'); + const {program, templateTypeChecker} = setup( + [ + { + fileName, + templates: {'Cmp': `
`}, + declarations: [ + { + name: 'GenericDir', + selector: '[genericDir]', + file: dirFile, + type: 'directive', + isGeneric: true + }, + ] + }, + { + fileName: dirFile, + source: ` + export class GenericDir{} + `, + templates: {}, + } + ], + ); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + + const nodes = templateTypeChecker.getTemplate(cmp)!; + + const symbol = templateTypeChecker.getSymbolOfNode(nodes[0] as TmplAstElement, cmp)!; + assertElementSymbol(symbol); + expect(symbol.directives.length).toBe(1); + const actualDirectives = + symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort(); + expect(actualDirectives).toEqual(['GenericDir']); + }); }); }); @@ -1254,6 +1386,10 @@ function assertElementSymbol(tSymbol: Symbol): asserts tSymbol is ElementSymbol expect(tSymbol.kind).toEqual(SymbolKind.Element); } +function assertDomBindingSymbol(tSymbol: Symbol): asserts tSymbol is DomBindingSymbol { + expect(tSymbol.kind).toEqual(SymbolKind.DomBinding); +} + export function setup(targets: TypeCheckingTarget[], config?: Partial) { return baseTestSetup( targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}}); diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index 3551396880..b8463d8f33 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -46,6 +46,9 @@ export interface DirectiveMeta { */ name: string; + /** The selector for the directive or `null` if there isn't one. */ + selector: string|null; + /** * Whether the directive is a component. */ diff --git a/packages/compiler/test/render3/view/binding_spec.ts b/packages/compiler/test/render3/view/binding_spec.ts index 6afb9c011d..5d792c338e 100644 --- a/packages/compiler/test/render3/view/binding_spec.ts +++ b/packages/compiler/test/render3/view/binding_spec.ts @@ -38,6 +38,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping(['ngForOf']), outputs: new IdentityInputMapping([]), isComponent: false, + selector: '[ngFor][ngForOf]', }); matcher.addSelectables(CssSelector.parse('[dir]'), { name: 'Dir', @@ -45,6 +46,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]), isComponent: false, + selector: '[dir]' }); matcher.addSelectables(CssSelector.parse('[hasOutput]'), { name: 'HasOutput', @@ -52,6 +54,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping(['outputBinding']), isComponent: false, + selector: '[hasOutput]' }); matcher.addSelectables(CssSelector.parse('[hasInput]'), { name: 'HasInput', @@ -59,6 +62,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping(['inputBinding']), outputs: new IdentityInputMapping([]), isComponent: false, + selector: '[hasInput]' }); return matcher; } @@ -103,6 +107,7 @@ describe('t2 binding', () => { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]), isComponent: false, + selector: 'text[dir]' }); const binder = new R3TargetBinder(matcher); const res = binder.bind({template: template.nodes});