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:
parent
494a2f3be4
commit
c74917a7d5
|
@ -104,6 +104,7 @@ runInEachFileSystem(() => {
|
|||
outputs: analysis.outputs,
|
||||
isComponent: false,
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
};
|
||||
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}});
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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});
|
||||
|
|
Loading…
Reference in New Issue