refactor(compiler-cli): update type checker symbols to include more information (#38844)

This commit updates the symbols in the TemplateTypeCheck API and methods
for retrieving them:

* Include `isComponent` and `selector` for directives so callers can determine which
attributes on an element map to the matched directives.
* Add a new `TextAttributeSymbol` and return this when requesting a symbol for a `TextAttribute`.
* When requesting a symbol for `PropertyWrite` and `MethodCall`, use the
`nameSpan` to retrieve symbols.
* Add fix to retrieve generic directives attached to elements/templates.

PR Close #38844
This commit is contained in:
Andrew Scott 2020-09-14 12:51:25 -07:00 committed by Alex Rickabaugh
parent 494a2f3be4
commit c74917a7d5
8 changed files with 275 additions and 63 deletions

View File

@ -104,6 +104,7 @@ runInEachFileSystem(() => {
outputs: analysis.outputs,
isComponent: false,
name: 'Dir',
selector: '[dir]',
};
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);

View File

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

View File

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

View File

@ -172,3 +172,8 @@ export function checkIfGenericTypesAreUnbound(node: ClassDeclaration<ts.ClassDec
}
return node.typeParameters.every(param => param.constraint === undefined);
}
export function isAccessExpression(node: ts.Node): node is ts.ElementAccessExpression|
ts.PropertyAccessExpression {
return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node);
}

View File

@ -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,

View File

@ -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 = `<div id="helloWorld"></div>`;
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 = `<div name="helloWorld"></div>`;
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': '<div (output)="lastEvent = $event"></div>'},
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': '<div [input]="toString(123)"></div>'},
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 = `<div dir [inputA]="'my input'"></div>`;
const templateString = `<div [name]="'my input'"></div>`;
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 = `<div dir [inputA]="'my input A'"></div>`;
@ -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': `<div genericDir></div>`},
declarations: [
{
name: 'GenericDir',
selector: '[genericDir]',
file: dirFile,
type: 'directive',
isGeneric: true
},
]
},
{
fileName: dirFile,
source: `
export class GenericDir<T>{}
`,
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<any>']);
});
});
});
@ -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<TypeCheckingConfig>) {
return baseTestSetup(
targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}});

View File

@ -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.
*/

View File

@ -38,6 +38,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
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<DirectiveMeta> {
inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping([]),
isComponent: false,
selector: '[dir]'
});
matcher.addSelectables(CssSelector.parse('[hasOutput]'), {
name: 'HasOutput',
@ -52,6 +54,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
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<DirectiveMeta> {
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});