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, outputs: analysis.outputs,
isComponent: false, isComponent: false,
name: 'Dir', name: 'Dir',
selector: '[dir]',
}; };
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta); matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);

View File

@ -21,13 +21,14 @@ export enum SymbolKind {
Element, Element,
Template, Template,
Expression, Expression,
DomBinding,
} }
/** /**
* A representation of an entity in the `TemplateAst`. * A representation of an entity in the `TemplateAst`.
*/ */
export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol| 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. */ /** Information about where a `ts.Node` can be found in the type check block shim file. */
export interface ShimLocation { 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. */ /** The location in the shim file for the variable that holds the type of the directive. */
shimLocation: ShimLocation; 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 * 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 * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system'; import {AbsoluteFsPath} from '../../file_system';
import {isAssignment} from '../../util/src/typescript'; 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 {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments';
import {TemplateData} from './context'; import {TemplateData} from './context';
import {isAccessExpression} from './ts_util';
import {TcbDirectiveOutputsOp} from './type_check_block'; import {TcbDirectiveOutputsOp} from './type_check_block';
/** /**
* A class which extracts information from a 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. * 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: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null;
getSymbol(node: AST|TmplAstNode): Symbol|null; getSymbol(node: AST|TmplAstNode): Symbol|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 // TODO(atscott): input and output bindings only return the first directive match but should
// return a list of bindings for all of them. // return a list of bindings for all of them.
return this.getSymbolOfInputBinding(node); return this.getSymbolOfInputBinding(node);
@ -85,24 +85,55 @@ export class SymbolBuilder {
private getDirectivesOfNode(element: TmplAstElement|TmplAstTemplate): DirectiveSymbol[] { private getDirectivesOfNode(element: TmplAstElement|TmplAstTemplate): DirectiveSymbol[] {
const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan; const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan;
const tcbSourceFile = this.typeCheckBlock.getSourceFile(); 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); hasExpressionIdentifier(tcbSourceFile, node, ExpressionIdentifier.DIRECTIVE);
const nodes = findAllMatchingNodes( const nodes = findAllMatchingNodes(
this.typeCheckBlock, {withSpan: elementSourceSpan, filter: isDirectiveDeclaration}); this.typeCheckBlock, {withSpan: elementSourceSpan, filter: isDirectiveDeclaration});
return nodes return nodes
.map(node => { .map(node => {
const symbol = this.getSymbolOfTsNode(node); const symbol = (ts.isIdentifier(node) && ts.isVariableDeclaration(node.parent)) ?
if (symbol === null || symbol.tsSymbol === null) { this.getSymbolOfVariableDeclaration(node.parent) :
this.getSymbolOfTsNode(node);
if (symbol === null || symbol.tsSymbol === null ||
symbol.tsSymbol.declarations.length === 0) {
return null; 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; return directiveSymbol;
}) })
.filter((d): d is DirectiveSymbol => d !== null); .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 { private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null {
// Outputs are a `ts.CallExpression` that look like one of the two: // Outputs are a `ts.CallExpression` that look like one of the two:
// * _outputHelper(_t1["outputField"]).subscribe(handler); // * _outputHelper(_t1["outputField"]).subscribe(handler);
@ -114,7 +145,8 @@ export class SymbolBuilder {
} }
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding); 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 // Bindings to element or template events produce `addEventListener` which
// we cannot get the field for. // we cannot get the field for.
return null; return null;
@ -130,12 +162,12 @@ export class SymbolBuilder {
} }
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess); const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
if (target === null) { if (target === null) {
return null; return null;
} }
const positionInShimFile = outputFieldAccess.argumentExpression.getStart(); const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
const tsType = this.typeChecker.getTypeAtLocation(node); const tsType = this.typeChecker.getTypeAtLocation(node);
return { return {
kind: SymbolKind.Output, kind: SymbolKind.Output,
@ -149,40 +181,30 @@ export class SymbolBuilder {
}; };
} }
private getSymbolOfInputBinding(attributeBinding: TmplAstBoundAttribute): InputBindingSymbol private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
|null { 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( const node = findFirstMatchingNode(
this.typeCheckBlock, {withSpan: attributeBinding.sourceSpan, filter: isAssignment}); this.typeCheckBlock, {withSpan: binding.sourceSpan, filter: isAssignment});
if (node === null) { if (node === null || !isAccessExpression(node.left)) {
return null; return null;
} }
let tsSymbol: ts.Symbol|undefined; const symbolInfo = this.getSymbolOfTsNode(node.left);
let positionInShimFile: number|null = null; if (symbolInfo === null || symbolInfo.tsSymbol === 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) {
return null; return null;
} }
const consumer = this.templateData.boundTarget.getConsumerOfBinding(attributeBinding); const target = this.getDirectiveSymbolForAccessExpression(node.left, consumer);
let target: ElementSymbol|TemplateSymbol|DirectiveSymbol|null;
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
target = this.getSymbol(consumer);
} else {
target = this.getDirectiveSymbolForAccessExpression(node.left);
}
if (target === null) { if (target === null) {
return null; return null;
} }
@ -190,17 +212,17 @@ export class SymbolBuilder {
return { return {
kind: SymbolKind.Input, kind: SymbolKind.Input,
bindings: [{ bindings: [{
...symbolInfo,
tsSymbol: symbolInfo.tsSymbol,
kind: SymbolKind.Binding, kind: SymbolKind.Binding,
tsSymbol,
tsType,
target, target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}], }],
}; };
} }
private getDirectiveSymbolForAccessExpression(node: ts.ElementAccessExpression| private getDirectiveSymbolForAccessExpression(
ts.PropertyAccessExpression): DirectiveSymbol|null { node: ts.ElementAccessExpression|ts.PropertyAccessExpression,
{isComponent, selector}: TypeCheckableDirectiveMeta): DirectiveSymbol|null {
// In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1. // In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1.
// The retrieved symbol for _t1 will be the variable declaration. // The retrieved symbol for _t1 will be the variable declaration.
const tsSymbol = this.typeChecker.getSymbolAtLocation(node.expression); const tsSymbol = this.typeChecker.getSymbolAtLocation(node.expression);
@ -228,6 +250,8 @@ export class SymbolBuilder {
tsSymbol: symbol.tsSymbol, tsSymbol: symbol.tsSymbol,
tsType: symbol.tsType, tsType: symbol.tsType,
shimLocation: symbol.shimLocation, shimLocation: symbol.shimLocation,
isComponent,
selector,
}; };
} }
@ -295,9 +319,14 @@ export class SymbolBuilder {
return this.getSymbol(expressionTarget); 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( let node = findFirstMatchingNode(
this.typeCheckBlock, this.typeCheckBlock, {withSpan, filter: (n: ts.Node): n is ts.Node => true});
{withSpan: expression.sourceSpan, filter: (n: ts.Node): n is ts.Node => true});
if (node === null) { if (node === null) {
return null; return null;
} }
@ -344,20 +373,20 @@ export class SymbolBuilder {
} }
let tsSymbol: ts.Symbol|undefined; let tsSymbol: ts.Symbol|undefined;
let positionInShimFile: number;
if (ts.isPropertyAccessExpression(node)) { if (ts.isPropertyAccessExpression(node)) {
tsSymbol = this.typeChecker.getSymbolAtLocation(node.name); tsSymbol = this.typeChecker.getSymbolAtLocation(node.name);
positionInShimFile = node.name.getStart(); } else if (ts.isElementAccessExpression(node)) {
tsSymbol = this.typeChecker.getSymbolAtLocation(node.argumentExpression);
} else { } else {
tsSymbol = this.typeChecker.getSymbolAtLocation(node); tsSymbol = this.typeChecker.getSymbolAtLocation(node);
positionInShimFile = node.getStart();
} }
const positionInShimFile = this.getShimPositionForNode(node);
const type = this.typeChecker.getTypeAtLocation(node); const type = this.typeChecker.getTypeAtLocation(node);
return { return {
// If we could not find a symbol, fall back to the symbol on the type for the node. // 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. // 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, tsSymbol: tsSymbol ?? type.symbol ?? null,
tsType: type, tsType: type,
shimLocation: {shimPath: this.shimPath, positionInShimFile}, shimLocation: {shimPath: this.shimPath, positionInShimFile},
@ -381,7 +410,20 @@ export class SymbolBuilder {
if (symbol === null) { if (symbol === null) {
return null; return null;
} }
return symbol; 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); 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, name: decl.name,
ref: new Reference(resolveDeclaration(decl)), ref: new Reference(resolveDeclaration(decl)),
exportAs: decl.exportAs || null, exportAs: decl.exportAs || null,
selector: decl.selector || null,
hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard || false, hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard || false,
inputs: ClassPropertyMapping.fromMappedObject(decl.inputs || {}), inputs: ClassPropertyMapping.fromMappedObject(decl.inputs || {}),
isComponent: decl.isComponent || false, isComponent: decl.isComponent || false,

View File

@ -12,13 +12,13 @@ import * as ts from 'typescript';
import {absoluteFrom, getSourceFileOrError} from '../../file_system'; import {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {ClassDeclaration} from '../../reflection'; 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'; import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
runInEachFileSystem(() => { runInEachFileSystem(() => {
describe('TemplateTypeChecker.getSymbolOfNode', () => { 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 fileName = absoluteFrom('/main.ts');
const templateString = `<div id="helloWorld"></div>`; const templateString = `<div id="helloWorld"></div>`;
const {templateTypeChecker, program} = setup( const {templateTypeChecker, program} = setup(
@ -34,8 +34,44 @@ runInEachFileSystem(() => {
const cmp = getClass(sf, 'Cmp'); const cmp = getClass(sf, 'Cmp');
const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; const {attributes} = getAstElements(templateTypeChecker, cmp)[0];
const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp); const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!;
expect(symbol).toBeNull(); 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', () => { describe('templates', () => {
@ -650,6 +686,54 @@ runInEachFileSystem(() => {
expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); 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', () => { describe('input bindings', () => {
@ -750,9 +834,9 @@ runInEachFileSystem(() => {
.toEqual('ngForOf'); .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 fileName = absoluteFrom('/main.ts');
const templateString = `<div dir [inputA]="'my input'"></div>`; const templateString = `<div [name]="'my input'"></div>`;
const {program, templateTypeChecker} = setup([ const {program, templateTypeChecker} = setup([
{fileName, templates: {'Cmp': templateString}, declarations: []}, {fileName, templates: {'Cmp': templateString}, declarations: []},
]); ]);
@ -762,11 +846,12 @@ runInEachFileSystem(() => {
const nodes = templateTypeChecker.getTemplate(cmp)!; const nodes = templateTypeChecker.getTemplate(cmp)!;
const binding = (nodes[0] as TmplAstElement).inputs[0]; const binding = (nodes[0] as TmplAstElement).inputs[0];
const symbol = templateTypeChecker.getSymbolOfNode(binding, cmp); const symbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
expect(symbol).toBeNull(); 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 fileName = absoluteFrom('/main.ts');
const dirFile = absoluteFrom('/dir.ts'); const dirFile = absoluteFrom('/dir.ts');
const templateString = `<div dir [inputA]="'my input A'"></div>`; const templateString = `<div dir [inputA]="'my input A'"></div>`;
@ -794,8 +879,9 @@ runInEachFileSystem(() => {
const nodes = templateTypeChecker.getTemplate(cmp)!; const nodes = templateTypeChecker.getTemplate(cmp)!;
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp); const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
expect(symbol).toBeNull(); assertDomBindingSymbol(symbol);
assertElementSymbol(symbol.host);
}); });
it('can match binding when there are two directives', () => { it('can match binding when there are two directives', () => {
@ -1120,6 +1206,7 @@ runInEachFileSystem(() => {
{ {
name: 'ChildComponent', name: 'ChildComponent',
selector: 'child-component', selector: 'child-component',
isComponent: true,
file: dirFile, file: dirFile,
type: 'directive', type: 'directive',
}, },
@ -1145,6 +1232,7 @@ runInEachFileSystem(() => {
assertDirectiveSymbol(symbol.directives[0]); assertDirectiveSymbol(symbol.directives[0]);
expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType)) expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType))
.toEqual('ChildComponent'); .toEqual('ChildComponent');
expect(symbol.directives[0].isComponent).toBe(true);
}); });
it('element with directive matches', () => { it('element with directive matches', () => {
@ -1199,8 +1287,52 @@ runInEachFileSystem(() => {
const actualDirectives = const actualDirectives =
symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort(); symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort();
expect(actualDirectives).toEqual(expectedDirectives); 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); 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>) { export function setup(targets: TypeCheckingTarget[], config?: Partial<TypeCheckingConfig>) {
return baseTestSetup( return baseTestSetup(
targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}}); targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}});

View File

@ -46,6 +46,9 @@ export interface DirectiveMeta {
*/ */
name: string; name: string;
/** The selector for the directive or `null` if there isn't one. */
selector: string|null;
/** /**
* Whether the directive is a component. * Whether the directive is a component.
*/ */

View File

@ -38,6 +38,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping(['ngForOf']), inputs: new IdentityInputMapping(['ngForOf']),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
selector: '[ngFor][ngForOf]',
}); });
matcher.addSelectables(CssSelector.parse('[dir]'), { matcher.addSelectables(CssSelector.parse('[dir]'), {
name: 'Dir', name: 'Dir',
@ -45,6 +46,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
selector: '[dir]'
}); });
matcher.addSelectables(CssSelector.parse('[hasOutput]'), { matcher.addSelectables(CssSelector.parse('[hasOutput]'), {
name: 'HasOutput', name: 'HasOutput',
@ -52,6 +54,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping(['outputBinding']), outputs: new IdentityInputMapping(['outputBinding']),
isComponent: false, isComponent: false,
selector: '[hasOutput]'
}); });
matcher.addSelectables(CssSelector.parse('[hasInput]'), { matcher.addSelectables(CssSelector.parse('[hasInput]'), {
name: 'HasInput', name: 'HasInput',
@ -59,6 +62,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping(['inputBinding']), inputs: new IdentityInputMapping(['inputBinding']),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
selector: '[hasInput]'
}); });
return matcher; return matcher;
} }
@ -103,6 +107,7 @@ describe('t2 binding', () => {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
selector: 'text[dir]'
}); });
const binder = new R3TargetBinder(matcher); const binder = new R3TargetBinder(matcher);
const res = binder.bind({template: template.nodes}); const res = binder.bind({template: template.nodes});