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,
|
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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}});
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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});
|
||||||
|
|
Loading…
Reference in New Issue