feat(compiler-cli): `TemplateTypeChecker` operation to get `Symbol` from a template node (#38618)
Specifically, this commit adds support for retrieving a `Symbol` from a `TmplAstBoundEvent` or `TmplAstBoundAttribute`. Other template nodes will be supported in following commits. PR Close #38618
This commit is contained in:
parent
a46e0e48a3
commit
c4556db9f5
|
@ -6,9 +6,11 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ParseError, TmplAstNode} from '@angular/compiler';
|
import {AST, ParseError, TmplAstNode,} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {Symbol} from './symbols';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
|
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
|
||||||
* compiler's understanding of component templates.
|
* compiler's understanding of component templates.
|
||||||
|
@ -77,6 +79,15 @@ export interface TemplateTypeChecker {
|
||||||
* This method always runs in `OptimizeFor.SingleFile` mode.
|
* This method always runs in `OptimizeFor.SingleFile` mode.
|
||||||
*/
|
*/
|
||||||
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null;
|
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a `Symbol` for the node in a component's template.
|
||||||
|
*
|
||||||
|
* This method can return `null` if a valid `Symbol` cannot be determined for the node.
|
||||||
|
*
|
||||||
|
* @see Symbol
|
||||||
|
*/
|
||||||
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ParseError, parseTemplate, TmplAstNode} from '@angular/compiler';
|
import {AST, ParseError, parseTemplate, TmplAstNode} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
|
@ -15,12 +15,13 @@ import {IncrementalBuild} from '../../incremental/api';
|
||||||
import {ReflectionHost} from '../../reflection';
|
import {ReflectionHost} from '../../reflection';
|
||||||
import {isShim} from '../../shims';
|
import {isShim} from '../../shims';
|
||||||
import {getSourceFileOrNull} from '../../util/src/typescript';
|
import {getSourceFileOrNull} from '../../util/src/typescript';
|
||||||
import {OptimizeFor, ProgramTypeCheckAdapter, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
import {TemplateDiagnostic} from '../diagnostics';
|
import {TemplateDiagnostic} from '../diagnostics';
|
||||||
|
|
||||||
import {InliningMode, ShimTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
||||||
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
||||||
import {TemplateSourceManager} from './source';
|
import {TemplateSourceManager} from './source';
|
||||||
|
import {SymbolBuilder} from './template_symbol_builder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary template type-checking engine, which performs type-checking using a
|
* Primary template type-checking engine, which performs type-checking using a
|
||||||
|
@ -50,6 +51,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||||
|
const templateData = this.getTemplateData(component);
|
||||||
|
if (templateData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return templateData.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTemplateData(component: ts.ClassDeclaration): TemplateData|null {
|
||||||
this.ensureShimForComponent(component);
|
this.ensureShimForComponent(component);
|
||||||
|
|
||||||
const sf = component.getSourceFile();
|
const sf = component.getSourceFile();
|
||||||
|
@ -59,7 +68,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
const fileRecord = this.getFileData(sfPath);
|
const fileRecord = this.getFileData(sfPath);
|
||||||
|
|
||||||
if (!fileRecord.shimData.has(shimPath)) {
|
if (!fileRecord.shimData.has(shimPath)) {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
||||||
|
@ -69,7 +78,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shimRecord.templates.get(templateId)!.template;
|
return shimRecord.templates.get(templateId)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||||
|
@ -349,6 +358,22 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
return this.state.get(path)!;
|
return this.state.get(path)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
|
||||||
|
const tcb = this.getTypeCheckBlock(component);
|
||||||
|
if (tcb === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeChecker = this.typeCheckingStrategy.getProgram().getTypeChecker();
|
||||||
|
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
|
||||||
|
const data = this.getTemplateData(component);
|
||||||
|
if (data === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SymbolBuilder(typeChecker, shimPath, tcb, data).getSymbol(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDiagnostic(
|
function convertDiagnostic(
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AbsoluteSourceSpan} from '@angular/compiler';
|
import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
const parseSpanComment = /^(\d+),(\d+)$/;
|
const parseSpanComment = /^(\d+),(\d+)$/;
|
||||||
|
@ -17,7 +17,8 @@ const parseSpanComment = /^(\d+),(\d+)$/;
|
||||||
*
|
*
|
||||||
* Will return `null` if no trailing comments on the node match the expected form of a source span.
|
* Will return `null` if no trailing comments on the node match the expected form of a source span.
|
||||||
*/
|
*/
|
||||||
export function readSpanComment(sourceFile: ts.SourceFile, node: ts.Node): AbsoluteSourceSpan|null {
|
export function readSpanComment(
|
||||||
|
node: ts.Node, sourceFile: ts.SourceFile = node.getSourceFile()): AbsoluteSourceSpan|null {
|
||||||
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||||
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,7 +52,7 @@ export function addExpressionIdentifier(node: ts.Node, identifier: ExpressionIde
|
||||||
/* hasTrailingNewLine */ false);
|
/* hasTrailingNewLine */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IGNORE_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`;
|
const IGNORE_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag the `ts.Node` with an indication that any errors arising from the evaluation of the node
|
* Tag the `ts.Node` with an indication that any errors arising from the evaluation of the node
|
||||||
|
@ -72,3 +73,60 @@ export function hasIgnoreMarker(node: ts.Node, sourceFile: ts.SourceFile): boole
|
||||||
return commentText === IGNORE_MARKER;
|
return commentText === IGNORE_MARKER;
|
||||||
}) === true;
|
}) === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeRecursiveVisitor<T extends ts.Node>(visitor: (node: ts.Node) => T | null):
|
||||||
|
(node: ts.Node) => T | undefined {
|
||||||
|
function recursiveVisitor(node: ts.Node): T|undefined {
|
||||||
|
const res = visitor(node);
|
||||||
|
return res !== null ? res : node.forEachChild(recursiveVisitor);
|
||||||
|
}
|
||||||
|
return recursiveVisitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindOptions<T extends ts.Node> {
|
||||||
|
filter: (node: ts.Node) => node is T;
|
||||||
|
withSpan?: AbsoluteSourceSpan|ParseSourceSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a `ts.Node` with finds the first node whose matching the criteria specified
|
||||||
|
* by the `FindOptions`.
|
||||||
|
*
|
||||||
|
* Returns `null` when no `ts.Node` matches the given conditions.
|
||||||
|
*/
|
||||||
|
export function findFirstMatchingNode<T extends ts.Node>(tcb: ts.Node, opts: FindOptions<T>): T|
|
||||||
|
null {
|
||||||
|
let withSpan: {start: number, end: number}|null = null;
|
||||||
|
if (opts.withSpan !== undefined) {
|
||||||
|
if (opts.withSpan instanceof AbsoluteSourceSpan) {
|
||||||
|
withSpan = opts.withSpan;
|
||||||
|
} else {
|
||||||
|
withSpan = {start: opts.withSpan.start.offset, end: opts.withSpan.end.offset};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sf = tcb.getSourceFile();
|
||||||
|
const visitor = makeRecursiveVisitor<T>(node => {
|
||||||
|
if (!opts.filter(node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (withSpan !== null) {
|
||||||
|
const comment = readSpanComment(node, sf);
|
||||||
|
if (comment === null || withSpan.start !== comment.start || withSpan.end !== comment.end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
return tcb.forEachChild(visitor) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasExpressionIdentifier(
|
||||||
|
sourceFile: ts.SourceFile, node: ts.Node, identifier: ExpressionIdentifier): boolean {
|
||||||
|
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||||
|
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const commentText = sourceFile.text.substring(pos + 2, end - 2);
|
||||||
|
return commentText === `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`;
|
||||||
|
}) || false;
|
||||||
|
}
|
||||||
|
|
|
@ -162,7 +162,7 @@ function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLoc
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const span = readSpanComment(sourceFile, node);
|
const span = readSpanComment(node, sourceFile);
|
||||||
if (span !== null) {
|
if (span !== null) {
|
||||||
// Once the positional information has been extracted, search further up the TCB to extract
|
// Once the positional information has been extracted, search further up the TCB to extract
|
||||||
// the unique id that is attached with the TCB's function declaration.
|
// the unique id that is attached with the TCB's function declaration.
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} 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, TsNodeSymbolInfo, VariableSymbol} from '../api';
|
||||||
|
|
||||||
|
import {ExpressionIdentifier, findFirstMatchingNode, hasExpressionIdentifier} from './comments';
|
||||||
|
import {TemplateData} from './context';
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export class SymbolBuilder {
|
||||||
|
constructor(
|
||||||
|
private readonly typeChecker: ts.TypeChecker, private readonly shimPath: AbsoluteFsPath,
|
||||||
|
private readonly typeCheckBlock: ts.Node, private readonly templateData: TemplateData) {}
|
||||||
|
|
||||||
|
getSymbol(node: AST|TmplAstNode): Symbol|null {
|
||||||
|
if (node instanceof TmplAstBoundAttribute) {
|
||||||
|
// 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);
|
||||||
|
} else if (node instanceof TmplAstBoundEvent) {
|
||||||
|
return this.getSymbolOfBoundEvent(node);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null {
|
||||||
|
// Outputs are a `ts.CallExpression` that look like one of the two:
|
||||||
|
// * _outputHelper(_t1["outputField"]).subscribe(handler);
|
||||||
|
// * _t1.addEventListener(handler);
|
||||||
|
const node = findFirstMatchingNode(
|
||||||
|
this.typeCheckBlock, {withSpan: eventBinding.sourceSpan, filter: ts.isCallExpression});
|
||||||
|
if (node === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
|
||||||
|
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
||||||
|
// Bindings to element or template events produce `addEventListener` which
|
||||||
|
// we cannot get the field for.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node);
|
||||||
|
if (outputFieldAccess === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tsSymbol = this.typeChecker.getSymbolAtLocation(outputFieldAccess.argumentExpression);
|
||||||
|
if (tsSymbol === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess);
|
||||||
|
if (target === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionInShimFile = outputFieldAccess.argumentExpression.getStart();
|
||||||
|
const tsType = this.typeChecker.getTypeAtLocation(node);
|
||||||
|
return {
|
||||||
|
kind: SymbolKind.Output,
|
||||||
|
bindings: [{
|
||||||
|
kind: SymbolKind.Binding,
|
||||||
|
tsSymbol,
|
||||||
|
tsType,
|
||||||
|
target,
|
||||||
|
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSymbolOfInputBinding(attributeBinding: TmplAstBoundAttribute): InputBindingSymbol
|
||||||
|
|null {
|
||||||
|
const node = findFirstMatchingNode(
|
||||||
|
this.typeCheckBlock, {withSpan: attributeBinding.sourceSpan, filter: isAssignment});
|
||||||
|
if (node === null) {
|
||||||
|
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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumer = this.templateData.boundTarget.getConsumerOfBinding(attributeBinding);
|
||||||
|
let target: ElementSymbol|DirectiveSymbol|null;
|
||||||
|
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
||||||
|
// TODO(atscott): handle bindings to elements and templates
|
||||||
|
target = null;
|
||||||
|
} else {
|
||||||
|
target = this.getDirectiveSymbolForAccessExpression(node.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: SymbolKind.Input,
|
||||||
|
bindings: [{
|
||||||
|
kind: SymbolKind.Binding,
|
||||||
|
tsSymbol,
|
||||||
|
tsType,
|
||||||
|
target,
|
||||||
|
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDirectiveSymbolForAccessExpression(node: ts.ElementAccessExpression|
|
||||||
|
ts.PropertyAccessExpression): 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);
|
||||||
|
if (tsSymbol === undefined || tsSymbol.declarations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [declaration] = tsSymbol.declarations;
|
||||||
|
if (!ts.isVariableDeclaration(declaration) ||
|
||||||
|
!hasExpressionIdentifier(
|
||||||
|
// The expression identifier could be on the type (for regular directives) or the name
|
||||||
|
// (for generic directives and the ctor op).
|
||||||
|
declaration.getSourceFile(), declaration.type ?? declaration.name,
|
||||||
|
ExpressionIdentifier.DIRECTIVE)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = this.getSymbolOfVariableDeclaration(declaration);
|
||||||
|
if (symbol === null || symbol.tsSymbol === null || symbol.tsType === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...symbol,
|
||||||
|
kind: SymbolKind.Directive,
|
||||||
|
tsSymbol: symbol.tsSymbol,
|
||||||
|
tsType: symbol.tsType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo|null {
|
||||||
|
while (ts.isParenthesizedExpression(node)) {
|
||||||
|
node = node.expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tsSymbol: ts.Symbol|undefined;
|
||||||
|
let positionInShimFile: number;
|
||||||
|
if (ts.isPropertyAccessExpression(node)) {
|
||||||
|
tsSymbol = this.typeChecker.getSymbolAtLocation(node.name);
|
||||||
|
positionInShimFile = node.name.getStart();
|
||||||
|
} else {
|
||||||
|
tsSymbol = this.typeChecker.getSymbolAtLocation(node);
|
||||||
|
positionInShimFile = node.getStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
tsSymbol: tsSymbol ?? type.symbol ?? null,
|
||||||
|
tsType: type,
|
||||||
|
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSymbolOfVariableDeclaration(declaration: ts.VariableDeclaration): TsNodeSymbolInfo
|
||||||
|
|null {
|
||||||
|
// Instead of returning the Symbol for the temporary variable, we want to get the `ts.Symbol`
|
||||||
|
// for:
|
||||||
|
// - The type reference for `var _t2: MyDir = xyz` (prioritize/trust the declared type)
|
||||||
|
// - The initializer for `var _t2 = _t1.index`.
|
||||||
|
if (declaration.type && ts.isTypeReferenceNode(declaration.type)) {
|
||||||
|
return this.getSymbolOfTsNode(declaration.type.typeName);
|
||||||
|
}
|
||||||
|
if (declaration.initializer === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = this.getSymbolOfTsNode(declaration.initializer);
|
||||||
|
if (symbol === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
}
|
|
@ -376,8 +376,8 @@ class TcbDirectiveTypeOp extends TcbOp {
|
||||||
const id = this.tcb.allocateId();
|
const id = this.tcb.allocateId();
|
||||||
|
|
||||||
const type = this.tcb.env.referenceType(this.dir.ref);
|
const type = this.tcb.env.referenceType(this.dir.ref);
|
||||||
addParseSpanInfo(type, this.node.startSourceSpan || this.node.sourceSpan);
|
|
||||||
addExpressionIdentifier(type, ExpressionIdentifier.DIRECTIVE);
|
addExpressionIdentifier(type, ExpressionIdentifier.DIRECTIVE);
|
||||||
|
addParseSpanInfo(type, this.node.startSourceSpan || this.node.sourceSpan);
|
||||||
this.scope.addStatement(tsDeclareVariable(id, type));
|
this.scope.addStatement(tsDeclareVariable(id, type));
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -477,6 +477,8 @@ class TcbDirectiveCtorOp extends TcbOp {
|
||||||
|
|
||||||
execute(): ts.Identifier {
|
execute(): ts.Identifier {
|
||||||
const id = this.tcb.allocateId();
|
const id = this.tcb.allocateId();
|
||||||
|
addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
|
||||||
|
addParseSpanInfo(id, this.node.startSourceSpan || this.node.sourceSpan);
|
||||||
|
|
||||||
const genericInputs = new Map<string, TcbDirectiveInput>();
|
const genericInputs = new Map<string, TcbDirectiveInput>();
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,16 @@ export function ngForDts(): TestFile {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ngForTypeCheckTarget(): TypeCheckingTarget {
|
||||||
|
const dts = ngForDts();
|
||||||
|
return {
|
||||||
|
...dts,
|
||||||
|
fileName: dts.name,
|
||||||
|
source: dts.contents,
|
||||||
|
templates: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
||||||
applyTemplateContextGuards: true,
|
applyTemplateContextGuards: true,
|
||||||
checkQueries: false,
|
checkQueries: false,
|
||||||
|
|
|
@ -0,0 +1,490 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {TmplAstBoundAttribute, TmplAstElement, TmplAstTemplate} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||||
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
|
import {InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TypeCheckingConfig} from '../api';
|
||||||
|
|
||||||
|
import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('TemplateTypeChecker.getSymbolOfNodeInComponentTemplate', () => {
|
||||||
|
describe('input bindings', () => {
|
||||||
|
it('can retrieve a symbol for an input binding', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const templateString =
|
||||||
|
`<div dir [inputA]="'my input A'" [inputBRenamed]="'my inputB'"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {inputA: 'inputA', inputB: 'inputBRenamed'},
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {inputA!: string; inputB!: string}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
|
||||||
|
assertInputBindingSymbol(aSymbol);
|
||||||
|
expect((aSymbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.name.getText())
|
||||||
|
.toEqual('inputA');
|
||||||
|
|
||||||
|
const inputBbinding = (nodes[0] as TmplAstElement).inputs[1];
|
||||||
|
const bSymbol = templateTypeChecker.getSymbolOfNode(inputBbinding, cmp)!;
|
||||||
|
// TODO(atscott): The BoundTarget is not assigning renamed properties correctly
|
||||||
|
// assertInputBindingSymbol(bSymbol);
|
||||||
|
// expect((bSymbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
// .name.getText())
|
||||||
|
// .toEqual('inputB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not retrieve a symbol for an input when undeclared', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const templateString = `<div dir [inputA]="'my input A'"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {inputA: 'inputA'},
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
|
||||||
|
expect(aSymbol).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can retrieve a symbol for an input of structural directive', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const templateString = `<div *ngFor="let user of users"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{fileName, templates: {'Cmp': templateString}, declarations: [ngForDeclaration()]},
|
||||||
|
ngForTypeCheckTarget(),
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const ngForOfBinding =
|
||||||
|
(nodes[0] as TmplAstTemplate).templateAttrs.find(a => a.name === 'ngForOf')! as
|
||||||
|
TmplAstBoundAttribute;
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding, cmp)!;
|
||||||
|
assertInputBindingSymbol(symbol);
|
||||||
|
expect(
|
||||||
|
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
|
||||||
|
.toEqual('ngForOf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty list when there is no directive registered for the binding', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const templateString = `<div dir [inputA]="'my input'"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{fileName, templates: {'Cmp': templateString}, declarations: []},
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
const binding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(binding, cmp);
|
||||||
|
expect(symbol).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty list 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>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {},
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp);
|
||||||
|
expect(symbol).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can match binding when there are two directives', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const templateString = `<div dir otherDir [inputA]="'my input A'"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {inputA: 'inputA'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OtherDir',
|
||||||
|
selector: '[otherDir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `
|
||||||
|
export class TestDir {inputA!: string;}
|
||||||
|
export class OtherDir {}
|
||||||
|
`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
|
||||||
|
assertInputBindingSymbol(symbol);
|
||||||
|
expect(
|
||||||
|
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
|
||||||
|
.toEqual('inputA');
|
||||||
|
expect((symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.parent.name?.text)
|
||||||
|
.toEqual('TestDir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first field match when directive maps same input to two fields', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': `<div dir [inputA]="'my input A'"></div>`},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {inputA: 'inputA', otherInputA: 'inputA'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `
|
||||||
|
export class TestDir {inputA!: string; otherInputA!: string;}
|
||||||
|
`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
|
||||||
|
assertInputBindingSymbol(symbol);
|
||||||
|
expect(
|
||||||
|
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
|
||||||
|
.toEqual('otherInputA');
|
||||||
|
expect((symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.parent.name?.text)
|
||||||
|
.toEqual('TestDir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first directive match when two directives have the same input', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const templateString = `<div dir otherDir [inputA]="'my input A'"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {inputA: 'inputA'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OtherDir',
|
||||||
|
selector: '[otherDir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
inputs: {otherDirInputA: 'inputA'},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `
|
||||||
|
export class TestDir {inputA!: string;}
|
||||||
|
export class OtherDir {otherDirInputA!: string;}
|
||||||
|
`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const inputAbinding = (nodes[0] as TmplAstElement).inputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!;
|
||||||
|
assertInputBindingSymbol(symbol);
|
||||||
|
expect(
|
||||||
|
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
|
||||||
|
.toEqual('inputA');
|
||||||
|
expect((symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.parent.name?.text)
|
||||||
|
.toEqual('TestDir');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('output bindings', () => {
|
||||||
|
it('should find symbol for output binding', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const templateString =
|
||||||
|
`<div dir (outputA)="handle($event)" (renamedOutputB)="handle($event)"></div>`;
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': templateString},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
outputs: {outputA: 'outputA', outputB: 'renamedOutputB'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `
|
||||||
|
export class TestDir {outputA!: EventEmitter<string>; outputB!: EventEmitter<string>}
|
||||||
|
`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
|
||||||
|
const aSymbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!;
|
||||||
|
assertOutputBindingSymbol(aSymbol);
|
||||||
|
expect((aSymbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.name.getText())
|
||||||
|
.toEqual('outputA');
|
||||||
|
|
||||||
|
const outputBBinding = (nodes[0] as TmplAstElement).outputs[1];
|
||||||
|
const bSymbol = templateTypeChecker.getSymbolOfNode(outputBBinding, cmp)!;
|
||||||
|
// TODO(atscott): The BoundTarget is not assigning renamed properties correctly
|
||||||
|
// assertOutputBindingSymbol(bSymbol);
|
||||||
|
// expect((bSymbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
// .name.getText())
|
||||||
|
// .toEqual('outputB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find symbol for output binding when there are multiple directives', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': `<div dir otherdir (outputA)="handle($event)"></div>`},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
outputs: {outputA: 'outputA'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OtherDir',
|
||||||
|
selector: '[otherdir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
outputs: {unusedOutput: 'unusedOutput'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `
|
||||||
|
export class TestDir {outputA!: EventEmitter<string>;}
|
||||||
|
export class OtherDir {unusedOutput!: EventEmitter<string>;}
|
||||||
|
`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!;
|
||||||
|
assertOutputBindingSymbol(symbol);
|
||||||
|
expect(
|
||||||
|
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
|
||||||
|
.toEqual('outputA');
|
||||||
|
expect((symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||||
|
.parent.name?.text)
|
||||||
|
.toEqual('TestDir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty list when binding does not match any directive output', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': `<div dir (doesNotExist)="handle($event)"></div>`},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
outputs: {outputA: 'outputA'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {outputA!: EventEmitter<string>;}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp);
|
||||||
|
expect(symbol).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty list when checkTypeOfOutputEvents is false', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const {program, templateTypeChecker} = setup(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': `<div dir (outputA)="handle($event)"></div>`},
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
outputs: {outputA: 'outputA'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {outputA!: EventEmitter<string>;}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{checkTypeOfOutputEvents: false});
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||||
|
|
||||||
|
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp);
|
||||||
|
// TODO(atscott): should type checker still generate the subscription in this case?
|
||||||
|
expect(symbol).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertInputBindingSymbol(tSymbol: Symbol): asserts tSymbol is InputBindingSymbol {
|
||||||
|
expect(tSymbol.kind).toEqual(SymbolKind.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertOutputBindingSymbol(tSymbol: Symbol): asserts tSymbol is OutputBindingSymbol {
|
||||||
|
expect(tSymbol.kind).toEqual(SymbolKind.Output);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup(targets: TypeCheckingTarget[], config?: Partial<TypeCheckingConfig>) {
|
||||||
|
return baseTestSetup(
|
||||||
|
targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}});
|
||||||
|
}
|
|
@ -139,6 +139,11 @@ export function resolveModuleName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true if the node is an assignment expression. */
|
||||||
|
export function isAssignment(node: ts.Node): node is ts.BinaryExpression {
|
||||||
|
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the keys `K` form a subset of the keys of `T`.
|
* Asserts that the keys `K` form a subset of the keys of `T`.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue