feat(ivy): let ngtsc annotate type check blocks with source positions (#30181)

The type check blocks (TCB) that ngtsc generates for achieving type
checking of Angular templates needs to be annotated with positional
information in order to translate TypeScript's diagnostics for the TCB
code back to the location in the user's template. This commit augments
the TCB by attaching trailing comments with AST nodes, such that a node
can be traced back to its source location.

PR Close #30181
This commit is contained in:
JoostK 2019-04-17 20:48:15 +02:00 committed by Miško Hevery
parent 8f3dd85600
commit 985513351b
6 changed files with 391 additions and 127 deletions

View File

@ -0,0 +1,74 @@
/**
* @license
* Copyright Google Inc. 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 {ParseSourceSpan, ParseSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {ClassDeclaration} from '../../reflection';
import {getSourceFile} from '../../util/src/typescript';
/**
* An `AbsoluteSpan` is the result of translating the `ParseSpan` of `AST` template expression nodes
* to their absolute positions, as the `ParseSpan` is always relative to the start of the
* expression, not the full template.
*/
export interface AbsoluteSpan {
__brand__: 'AbsoluteSpan';
start: number;
end: number;
}
/**
* Translates a `ParseSpan` into an `AbsoluteSpan` by incorporating the location information that
* the `ParseSourceSpan` represents.
*/
export function toAbsoluteSpan(span: ParseSpan, sourceSpan: ParseSourceSpan): AbsoluteSpan {
const offset = sourceSpan.start.offset;
return <AbsoluteSpan>{start: span.start + offset, end: span.end + offset};
}
/**
* Wraps the node in parenthesis such that inserted span comments become attached to the proper
* node. This is an alias for `ts.createParen` with the benefit that it signifies that the
* inserted parenthesis are for diagnostic purposes, not for correctness of the rendered TCB code.
*
* Note that it is important that nodes and its attached comment are not wrapped into parenthesis
* by default, as it prevents correct translation of e.g. diagnostics produced for incorrect method
* arguments. Such diagnostics would then be produced for the parenthesised node whereas the
* positional comment would be located within that node, resulting in a mismatch.
*/
export function wrapForDiagnostics(expr: ts.Expression): ts.Expression {
return ts.createParen(expr);
}
/**
* Adds a synthetic comment to the expression that represents the parse span of the provided node.
* This comment can later be retrieved as trivia of a node to recover original source locations.
*/
export function addParseSpanInfo(node: ts.Node, span: AbsoluteSpan | ParseSourceSpan): void {
let commentText: string;
if (typeof span.start === 'number') {
commentText = `${span.start},${span.end}`;
} else {
const {start, end} = span as ParseSourceSpan;
commentText = `${start.offset},${end.offset}`;
}
ts.addSyntheticTrailingComment(
node, ts.SyntaxKind.MultiLineCommentTrivia, commentText,
/* hasTrailingNewLine */ false);
}
/**
* Adds a synthetic comment to the function declaration that contains the source location
* of the class declaration.
*/
export function addSourceInfo(
tcb: ts.FunctionDeclaration, source: ClassDeclaration<ts.ClassDeclaration>): void {
const fileName = getSourceFile(source).fileName;
const commentText = `${fileName}#${source.name.text}`;
ts.addSyntheticLeadingComment(tcb, ts.SyntaxKind.MultiLineCommentTrivia, commentText, true);
}

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler'; import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TypeCheckingConfig} from './api'; import {TypeCheckingConfig} from './api';
import {AbsoluteSpan, addParseSpanInfo, wrapForDiagnostics} from './diagnostics';
const NULL_AS_ANY = const NULL_AS_ANY =
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
@ -40,16 +41,17 @@ const BINARY_OPS = new Map<string, ts.SyntaxKind>([
* AST. * AST.
*/ */
export function astToTypescript( export function astToTypescript(
ast: AST, maybeResolve: (ast: AST) => ts.Expression | null, ast: AST, maybeResolve: (ast: AST) => (ts.Expression | null), config: TypeCheckingConfig,
config: TypeCheckingConfig): ts.Expression { translateSpan: (span: ParseSpan) => AbsoluteSpan): ts.Expression {
const translator = new AstTranslator(maybeResolve, config); const translator = new AstTranslator(maybeResolve, config, translateSpan);
return translator.translate(ast); return translator.translate(ast);
} }
class AstTranslator implements AstVisitor { class AstTranslator implements AstVisitor {
constructor( constructor(
private maybeResolve: (ast: AST) => ts.Expression | null, private maybeResolve: (ast: AST) => (ts.Expression | null),
private config: TypeCheckingConfig) {} private config: TypeCheckingConfig,
private translateSpan: (span: ParseSpan) => AbsoluteSpan) {}
translate(ast: AST): ts.Expression { translate(ast: AST): ts.Expression {
// Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`, // Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`,
@ -68,13 +70,15 @@ class AstTranslator implements AstVisitor {
} }
visitBinary(ast: Binary): ts.Expression { visitBinary(ast: Binary): ts.Expression {
const lhs = this.translate(ast.left); const lhs = wrapForDiagnostics(this.translate(ast.left));
const rhs = this.translate(ast.right); const rhs = wrapForDiagnostics(this.translate(ast.right));
const op = BINARY_OPS.get(ast.operation); const op = BINARY_OPS.get(ast.operation);
if (op === undefined) { if (op === undefined) {
throw new Error(`Unsupported Binary.operation: ${ast.operation}`); throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
} }
return ts.createBinary(lhs, op as any, rhs); const node = ts.createBinary(lhs, op as any, rhs);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitChain(ast: Chain): never { throw new Error('Method not implemented.'); } visitChain(ast: Chain): never { throw new Error('Method not implemented.'); }
@ -83,7 +87,9 @@ class AstTranslator implements AstVisitor {
const condExpr = this.translate(ast.condition); const condExpr = this.translate(ast.condition);
const trueExpr = this.translate(ast.trueExp); const trueExpr = this.translate(ast.trueExp);
const falseExpr = this.translate(ast.falseExp); const falseExpr = this.translate(ast.falseExp);
return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr)); const node = ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitFunctionCall(ast: FunctionCall): never { throw new Error('Method not implemented.'); } visitFunctionCall(ast: FunctionCall): never { throw new Error('Method not implemented.'); }
@ -102,16 +108,20 @@ class AstTranslator implements AstVisitor {
} }
visitKeyedRead(ast: KeyedRead): ts.Expression { visitKeyedRead(ast: KeyedRead): ts.Expression {
const receiver = this.translate(ast.obj); const receiver = wrapForDiagnostics(this.translate(ast.obj));
const key = this.translate(ast.key); const key = this.translate(ast.key);
return ts.createElementAccess(receiver, key); const node = ts.createElementAccess(receiver, key);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitKeyedWrite(ast: KeyedWrite): never { throw new Error('Method not implemented.'); } visitKeyedWrite(ast: KeyedWrite): never { throw new Error('Method not implemented.'); }
visitLiteralArray(ast: LiteralArray): ts.Expression { visitLiteralArray(ast: LiteralArray): ts.Expression {
const elements = ast.expressions.map(expr => this.translate(expr)); const elements = ast.expressions.map(expr => this.translate(expr));
return ts.createArrayLiteral(elements); const node = ts.createArrayLiteral(elements);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitLiteralMap(ast: LiteralMap): ts.Expression { visitLiteralMap(ast: LiteralMap): ts.Expression {
@ -119,42 +129,56 @@ class AstTranslator implements AstVisitor {
const value = this.translate(ast.values[idx]); const value = this.translate(ast.values[idx]);
return ts.createPropertyAssignment(ts.createStringLiteral(key), value); return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
}); });
return ts.createObjectLiteral(properties, true); const node = ts.createObjectLiteral(properties, true);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression { visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression {
let node: ts.Expression;
if (ast.value === undefined) { if (ast.value === undefined) {
return ts.createIdentifier('undefined'); node = ts.createIdentifier('undefined');
} else if (ast.value === null) { } else if (ast.value === null) {
return ts.createNull(); node = ts.createNull();
} else { } else {
return ts.createLiteral(ast.value); node = ts.createLiteral(ast.value);
} }
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitMethodCall(ast: MethodCall): ts.Expression { visitMethodCall(ast: MethodCall): ts.Expression {
const receiver = this.translate(ast.receiver); const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const method = ts.createPropertyAccess(receiver, ast.name); const method = ts.createPropertyAccess(receiver, ast.name);
const args = ast.args.map(expr => this.translate(expr)); const args = ast.args.map(expr => this.translate(expr));
return ts.createCall(method, undefined, args); const node = ts.createCall(method, undefined, args);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitNonNullAssert(ast: NonNullAssert): ts.Expression { visitNonNullAssert(ast: NonNullAssert): ts.Expression {
const expr = this.translate(ast.expression); const expr = wrapForDiagnostics(this.translate(ast.expression));
return ts.createNonNullExpression(expr); const node = ts.createNonNullExpression(expr);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitPipe(ast: BindingPipe): never { throw new Error('Method not implemented.'); } visitPipe(ast: BindingPipe): never { throw new Error('Method not implemented.'); }
visitPrefixNot(ast: PrefixNot): ts.Expression { visitPrefixNot(ast: PrefixNot): ts.Expression {
return ts.createLogicalNot(this.translate(ast.expression)); const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.createLogicalNot(expression);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitPropertyRead(ast: PropertyRead): ts.Expression { visitPropertyRead(ast: PropertyRead): ts.Expression {
// This is a normal property read - convert the receiver to an expression and emit the correct // This is a normal property read - convert the receiver to an expression and emit the correct
// TypeScript expression to read the property. // TypeScript expression to read the property.
const receiver = this.translate(ast.receiver); const receiver = wrapForDiagnostics(this.translate(ast.receiver));
return ts.createPropertyAccess(receiver, ast.name); const node = ts.createPropertyAccess(receiver, ast.name);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitPropertyWrite(ast: PropertyWrite): never { throw new Error('Method not implemented.'); } visitPropertyWrite(ast: PropertyWrite): never { throw new Error('Method not implemented.'); }
@ -164,12 +188,14 @@ class AstTranslator implements AstVisitor {
visitSafeMethodCall(ast: SafeMethodCall): ts.Expression { visitSafeMethodCall(ast: SafeMethodCall): ts.Expression {
// See the comment in SafePropertyRead above for an explanation of the need for the non-null // See the comment in SafePropertyRead above for an explanation of the need for the non-null
// assertion here. // assertion here.
const receiver = this.translate(ast.receiver); const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
const args = ast.args.map(expr => this.translate(expr)); const args = ast.args.map(expr => this.translate(expr));
const expr = ts.createCall(method, undefined, args); const expr = ts.createCall(method, undefined, args);
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
return safeTernary(receiver, expr, whenNull); const node = safeTernary(receiver, expr, whenNull);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
visitSafePropertyRead(ast: SafePropertyRead): ts.Expression { visitSafePropertyRead(ast: SafePropertyRead): ts.Expression {
@ -177,10 +203,12 @@ class AstTranslator implements AstVisitor {
// whenNull is either of type 'any' or or 'undefined' depending on strictness. The non-null // whenNull is either of type 'any' or or 'undefined' depending on strictness. The non-null
// assertion is necessary because in practice 'a' may be a method call expression, which won't // assertion is necessary because in practice 'a' may be a method call expression, which won't
// have a narrowed type when repeated in the ternary true branch. // have a narrowed type when repeated in the ternary true branch.
const receiver = this.translate(ast.receiver); const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
return safeTernary(receiver, expr, whenNull); const node = safeTernary(receiver, expr, whenNull);
addParseSpanInfo(node, this.translateSpan(ast.span));
return node;
} }
} }

View File

@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BindingPipe, BindingType, BoundTarget, ImplicitReceiver, MethodCall, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; import {AST, BindingPipe, BindingType, BoundTarget, ImplicitReceiver, MethodCall, ParseSourceSpan, ParseSpan, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
import {addParseSpanInfo, addSourceInfo, toAbsoluteSpan, wrapForDiagnostics} from './diagnostics';
import {Environment} from './environment'; import {Environment} from './environment';
import {astToTypescript} from './expression'; import {astToTypescript} from './expression';
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util'; import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util';
@ -50,16 +51,17 @@ export function generateTypeCheckBlock(
// Wrap the body in an "if (true)" expression. This is unnecessary but has the effect of causing // Wrap the body in an "if (true)" expression. This is unnecessary but has the effect of causing
// the `ts.Printer` to format the type-check block nicely. // the `ts.Printer` to format the type-check block nicely.
const body = ts.createBlock([ts.createIf(ts.createTrue(), innerBody, undefined)]); const body = ts.createBlock([ts.createIf(ts.createTrue(), innerBody, undefined)]);
const fnDecl = ts.createFunctionDeclaration(
return ts.createFunctionDeclaration( /* decorators */ undefined,
/* decorators */ undefined, /* modifiers */ undefined,
/* modifiers */ undefined, /* asteriskToken */ undefined,
/* asteriskToken */ undefined, /* name */ name,
/* name */ name, /* typeParameters */ ref.node.typeParameters,
/* typeParameters */ ref.node.typeParameters, /* parameters */ paramList,
/* parameters */ paramList, /* type */ undefined,
/* type */ undefined, /* body */ body);
/* body */ body); addSourceInfo(fnDecl, ref.node);
return fnDecl;
} }
/** /**
@ -95,7 +97,9 @@ class TcbElementOp extends TcbOp {
execute(): ts.Identifier { execute(): ts.Identifier {
const id = this.tcb.allocateId(); const id = this.tcb.allocateId();
// Add the declaration of the element using document.createElement. // Add the declaration of the element using document.createElement.
this.scope.addStatement(tsCreateVariable(id, tsCreateElement(this.element.name))); const initializer = tsCreateElement(this.element.name);
addParseSpanInfo(initializer, this.element.startSourceSpan || this.element.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id; return id;
} }
} }
@ -123,6 +127,7 @@ class TcbVariableOp extends TcbOp {
const initializer = ts.createPropertyAccess( const initializer = ts.createPropertyAccess(
/* expression */ ctx, /* expression */ ctx,
/* name */ this.variable.value || '$implicit'); /* name */ this.variable.value || '$implicit');
addParseSpanInfo(initializer, this.variable.sourceSpan);
// Declare the variable, and return its identifier. // Declare the variable, and return its identifier.
this.scope.addStatement(tsCreateVariable(id, initializer)); this.scope.addStatement(tsCreateVariable(id, initializer));
@ -193,7 +198,7 @@ class TcbTemplateBodyOp extends TcbOp {
i instanceof TmplAstBoundAttribute && i.name === guard.inputName); i instanceof TmplAstBoundAttribute && i.name === guard.inputName);
if (boundInput !== undefined) { if (boundInput !== undefined) {
// If there is such a binding, generate an expression for it. // If there is such a binding, generate an expression for it.
const expr = tcbExpression(boundInput.value, this.tcb, this.scope); const expr = tcbExpression(boundInput.value, this.tcb, this.scope, boundInput.sourceSpan);
if (guard.type === 'binding') { if (guard.type === 'binding') {
// Use the binding expression itself as guard. // Use the binding expression itself as guard.
@ -205,6 +210,8 @@ class TcbTemplateBodyOp extends TcbOp {
dirInstId, dirInstId,
expr, expr,
]); ]);
addParseSpanInfo(
guardInvoke, toAbsoluteSpan(boundInput.value.span, boundInput.sourceSpan));
directiveGuards.push(guardInvoke); directiveGuards.push(guardInvoke);
} }
} }
@ -215,6 +222,7 @@ class TcbTemplateBodyOp extends TcbOp {
if (dir.hasNgTemplateContextGuard && this.tcb.env.config.applyTemplateContextGuards) { if (dir.hasNgTemplateContextGuard && this.tcb.env.config.applyTemplateContextGuards) {
const ctx = this.scope.resolve(this.template); const ctx = this.scope.resolve(this.template);
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]); const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
addParseSpanInfo(guardInvoke, this.template.sourceSpan);
directiveGuards.push(guardInvoke); directiveGuards.push(guardInvoke);
} }
} }
@ -254,7 +262,7 @@ class TcbTextInterpolationOp extends TcbOp {
} }
execute(): null { execute(): null {
const expr = tcbExpression(this.binding.value, this.tcb, this.scope); const expr = tcbExpression(this.binding.value, this.tcb, this.scope, this.binding.sourceSpan);
this.scope.addStatement(ts.createExpressionStatement(expr)); this.scope.addStatement(ts.createExpressionStatement(expr));
return null; return null;
} }
@ -282,6 +290,7 @@ class TcbDirectiveOp extends TcbOp {
// Call the type constructor of the directive to infer a type, and assign the directive // Call the type constructor of the directive to infer a type, and assign the directive
// instance. // instance.
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, bindings); const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, bindings);
addParseSpanInfo(typeCtor, this.node.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, typeCtor)); this.scope.addStatement(tsCreateVariable(id, typeCtor));
return id; return id;
} }
@ -326,7 +335,7 @@ class TcbUnclaimedInputsOp extends TcbOp {
continue; continue;
} }
let expr = tcbExpression(binding.value, this.tcb, this.scope); let expr = tcbExpression(binding.value, this.tcb, this.scope, binding.sourceSpan);
// If checking the type of bindings is disabled, cast the resulting expression to 'any' before // If checking the type of bindings is disabled, cast the resulting expression to 'any' before
// the assignment. // the assignment.
@ -339,8 +348,9 @@ class TcbUnclaimedInputsOp extends TcbOp {
// A direct binding to a property. // A direct binding to a property.
const propertyName = ATTR_TO_PROP[binding.name] || binding.name; const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
const prop = ts.createPropertyAccess(elId, propertyName); const prop = ts.createPropertyAccess(elId, propertyName);
const assign = ts.createBinary(prop, ts.SyntaxKind.EqualsToken, expr); const stmt = ts.createBinary(prop, ts.SyntaxKind.EqualsToken, wrapForDiagnostics(expr));
this.scope.addStatement(ts.createStatement(assign)); addParseSpanInfo(stmt, binding.sourceSpan);
this.scope.addStatement(ts.createExpressionStatement(stmt));
} else { } else {
this.scope.addStatement(ts.createExpressionStatement(expr)); this.scope.addStatement(ts.createExpressionStatement(expr));
} }
@ -686,11 +696,15 @@ function tcbCtxParam(
* Process an `AST` expression and convert it into a `ts.Expression`, generating references to the * Process an `AST` expression and convert it into a `ts.Expression`, generating references to the
* correct identifiers in the current scope. * correct identifiers in the current scope.
*/ */
function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression { function tcbExpression(
ast: AST, tcb: Context, scope: Scope, sourceSpan: ParseSourceSpan): ts.Expression {
const translateSpan = (span: ParseSpan) => toAbsoluteSpan(span, sourceSpan);
// `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed which // `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed which
// interprets specific expression nodes that interact with the `ImplicitReceiver`. These nodes // interprets specific expression nodes that interact with the `ImplicitReceiver`. These nodes
// actually refer to identifiers within the current scope. // actually refer to identifiers within the current scope.
return astToTypescript(ast, (ast) => tcbResolve(ast, tcb, scope), tcb.env.config); return astToTypescript(
ast, (ast) => tcbResolve(ast, tcb, scope, sourceSpan), tcb.env.config, translateSpan);
} }
/** /**
@ -703,11 +717,13 @@ function tcbCallTypeCtor(
// Construct an array of `ts.PropertyAssignment`s for each input of the directive that has a // Construct an array of `ts.PropertyAssignment`s for each input of the directive that has a
// matching binding. // matching binding.
const members = bindings.map(({field, expression}) => { const members = bindings.map(({field, expression, sourceSpan}) => {
if (!tcb.env.config.checkTypeOfBindings) { if (!tcb.env.config.checkTypeOfBindings) {
expression = tsCastToAny(expression); expression = tsCastToAny(expression);
} }
return ts.createPropertyAssignment(field, expression); const assignment = ts.createPropertyAssignment(field, wrapForDiagnostics(expression));
addParseSpanInfo(assignment, sourceSpan);
return assignment;
}); });
// Call the `ngTypeCtor` method on the directive class, with an object literal argument created // Call the `ngTypeCtor` method on the directive class, with an object literal argument created
@ -722,6 +738,7 @@ interface TcbBinding {
field: string; field: string;
property: string; property: string;
expression: ts.Expression; expression: ts.Expression;
sourceSpan: ParseSourceSpan;
} }
function tcbGetInputBindingExpressions( function tcbGetInputBindingExpressions(
@ -751,12 +768,13 @@ function tcbGetInputBindingExpressions(
function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void { function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void {
if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) { if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) {
// Produce an expression representing the value of the binding. // Produce an expression representing the value of the binding.
const expr = tcbExpression(attr.value, tcb, scope); const expr = tcbExpression(attr.value, tcb, scope, attr.sourceSpan);
// Call the callback. // Call the callback.
bindings.push({ bindings.push({
property: attr.name, property: attr.name,
field: propMatch.get(attr.name) !, field: propMatch.get(attr.name) !,
expression: expr, expression: expr,
sourceSpan: attr.sourceSpan,
}); });
} }
} }
@ -768,7 +786,8 @@ function tcbGetInputBindingExpressions(
* Some `AST` expressions refer to top-level concepts (references, variables, the component * Some `AST` expressions refer to top-level concepts (references, variables, the component
* context). This method assists in resolving those. * context). This method assists in resolving those.
*/ */
function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null { function tcbResolve(
ast: AST, tcb: Context, scope: Scope, sourceSpan: ParseSourceSpan): ts.Expression|null {
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) { if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
// Check whether the template metadata has bound a target for this expression. If so, then // Check whether the template metadata has bound a target for this expression. If so, then
// resolve that target. If not, then the expression is referencing the top-level component // resolve that target. If not, then the expression is referencing the top-level component
@ -777,7 +796,9 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
if (binding !== null) { if (binding !== null) {
// This expression has a binding to some variable or reference in the template. Resolve it. // This expression has a binding to some variable or reference in the template. Resolve it.
if (binding instanceof TmplAstVariable) { if (binding instanceof TmplAstVariable) {
return scope.resolve(binding); const expr = ts.getMutableClone(scope.resolve(binding));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, sourceSpan));
return expr;
} else if (binding instanceof TmplAstReference) { } else if (binding instanceof TmplAstReference) {
const target = tcb.boundTarget.getReferenceTarget(binding); const target = tcb.boundTarget.getReferenceTarget(binding);
if (target === null) { if (target === null) {
@ -788,7 +809,9 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
// element or template. // element or template.
if (target instanceof TmplAstElement) { if (target instanceof TmplAstElement) {
return scope.resolve(target); const expr = ts.getMutableClone(scope.resolve(target));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, sourceSpan));
return expr;
} else if (target instanceof TmplAstTemplate) { } else if (target instanceof TmplAstTemplate) {
// Direct references to an <ng-template> node simply require a value of type // Direct references to an <ng-template> node simply require a value of type
// `TemplateRef<any>`. To get this, an expression of the form // `TemplateRef<any>`. To get this, an expression of the form
@ -797,9 +820,12 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
value = ts.createAsExpression(value, tcb.env.referenceCoreType('TemplateRef', 1)); value = ts.createAsExpression(value, tcb.env.referenceCoreType('TemplateRef', 1));
value = ts.createParen(value); value = ts.createParen(value);
addParseSpanInfo(value, toAbsoluteSpan(ast.span, sourceSpan));
return value; return value;
} else { } else {
return scope.resolve(target.node, target.directive); const expr = ts.getMutableClone(scope.resolve(target.node, target.directive));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, sourceSpan));
return expr;
} }
} else { } else {
throw new Error(`Unreachable: ${binding}`); throw new Error(`Unreachable: ${binding}`);
@ -823,7 +849,7 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
// the component context itself. // the component context itself.
return ts.createIdentifier('ctx'); return ts.createIdentifier('ctx');
} else if (ast instanceof BindingPipe) { } else if (ast instanceof BindingPipe) {
const expr = tcbExpression(ast.exp, tcb, scope); const expr = tcbExpression(ast.exp, tcb, scope, sourceSpan);
let pipe: ts.Expression; let pipe: ts.Expression;
if (tcb.env.config.checkTypeOfPipes) { if (tcb.env.config.checkTypeOfPipes) {
pipe = tcb.getPipeByName(ast.name); pipe = tcb.getPipeByName(ast.name);
@ -831,15 +857,19 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
pipe = ts.createParen(ts.createAsExpression( pipe = ts.createParen(ts.createAsExpression(
ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
} }
const args = ast.args.map(arg => tcbExpression(arg, tcb, scope)); const args = ast.args.map(arg => tcbExpression(arg, tcb, scope, sourceSpan));
return tsCallMethod(pipe, 'transform', [expr, ...args]); const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
addParseSpanInfo(result, toAbsoluteSpan(ast.span, sourceSpan));
return result;
} else if ( } else if (
ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver && ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver &&
ast.name === '$any' && ast.args.length === 1) { ast.name === '$any' && ast.args.length === 1) {
const expr = tcbExpression(ast.args[0], tcb, scope); const expr = tcbExpression(ast.args[0], tcb, scope, sourceSpan);
const exprAsAny = const exprAsAny =
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
return ts.createParen(exprAsAny); const result = ts.createParen(exprAsAny);
addParseSpanInfo(result, toAbsoluteSpan(ast.span, sourceSpan));
return result;
} else { } else {
// This AST isn't special after all. // This AST isn't special after all.
return null; return null;

View File

@ -0,0 +1,128 @@
/**
* @license
* Copyright Google Inc. 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 {TestDeclaration, tcb} from './test_utils';
describe('type check blocks diagnostics', () => {
describe('parse spans', () => {
it('should annotate binary ops', () => {
expect(tcbWithSpans('{{ a + b }}'))
.toContain('"" + (((ctx).a /*3,5*/) + ((ctx).b /*7,9*/) /*3,9*/);');
});
it('should annotate conditions', () => {
expect(tcbWithSpans('{{ a ? b : c }}'))
.toContain('((ctx).a /*3,5*/ ? (ctx).b /*7,9*/ : (ctx).c /*11,13*/) /*3,13*/;');
});
it('should annotate interpolations', () => {
expect(tcbWithSpans('{{ hello }} {{ world }}'))
.toContain('"" + (ctx).hello /*3,9*/ + (ctx).world /*15,21*/;');
});
it('should annotate literal map expressions', () => {
// The additional method call is present to avoid that the object literal is emitted as
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({foo: a, bar: b}) }}';
expect(tcbWithSpans(TEMPLATE))
.toContain('m({ "foo": (ctx).a /*11,12*/, "bar": (ctx).b /*19,20*/ } /*5,21*/)');
});
it('should annotate literal array expressions', () => {
const TEMPLATE = '{{ [a, b] }}';
expect(tcbWithSpans(TEMPLATE)).toContain('[(ctx).a /*4,5*/, (ctx).b /*7,8*/] /*3,10*/;');
});
it('should annotate literals', () => {
const TEMPLATE = '{{ 123 }}';
expect(tcbWithSpans(TEMPLATE)).toContain('123 /*3,7*/;');
});
it('should annotate non-null assertions', () => {
const TEMPLATE = `{{ a! }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*3,4*/)! /*3,6*/);');
});
it('should annotate prefix not', () => {
const TEMPLATE = `{{ !a }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('!((ctx).a /*4,6*/) /*3,6*/;');
});
it('should annotate method calls', () => {
const TEMPLATE = `{{ method(a, b) }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain('(ctx).method((ctx).a /*10,11*/, (ctx).b /*13,14*/) /*3,16*/;');
});
it('should annotate property access', () => {
const TEMPLATE = `{{ a.b.c }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('(((ctx).a /*3,4*/).b /*3,6*/).c /*3,9*/;');
});
it('should annotate keyed property access', () => {
const TEMPLATE = `{{ a[b] }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('((ctx).a /*3,4*/)[(ctx).b /*5,6*/] /*3,8*/;');
});
it('should annotate safe property access', () => {
const TEMPLATE = `{{ a?.b }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain('(((ctx).a /*3,4*/) != null ? ((ctx).a /*3,4*/)!.b : undefined) /*3,8*/;');
});
it('should annotate safe method calls', () => {
const TEMPLATE = `{{ a?.method(b) }}`;
expect(tcbWithSpans(TEMPLATE))
.toContain(
'(((ctx).a /*3,4*/) != null ? ((ctx).a /*3,4*/)!.method((ctx).b /*13,14*/) : undefined) /*3,16*/;');
});
it('should annotate $any casts', () => {
const TEMPLATE = `{{ $any(a) }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('((ctx).a /*8,9*/ as any) /*3,11*/;');
});
it('should annotate pipe usages', () => {
const TEMPLATE = `{{ a | test:b }}`;
const PIPES: TestDeclaration[] = [{
type: 'pipe',
name: 'TestPipe',
pipeName: 'test',
}];
const block = tcbWithSpans(TEMPLATE, PIPES);
expect(block).toContain(
'(null as TestPipe).transform((ctx).a /*3,5*/, (ctx).b /*12,14*/) /*3,14*/;');
});
describe('attaching multiple comments for multiple references', () => {
it('should be correct for element refs', () => {
const TEMPLATE = `<span #a></span>{{ a || a }}`;
expect(tcbWithSpans(TEMPLATE)).toContain('((_t1 /*19,21*/) || (_t1 /*24,26*/) /*19,26*/);');
});
it('should be correct for template vars', () => {
const TEMPLATE = `<ng-template let-a="b">{{ a || a }}</ng-template>`;
expect(tcbWithSpans(TEMPLATE)).toContain('((_t2 /*26,28*/) || (_t2 /*31,33*/) /*26,33*/);');
});
it('should be correct for directive refs', () => {
const DIRECTIVES: TestDeclaration[] = [{
type: 'directive',
name: 'MyComponent',
selector: 'my-cmp',
isComponent: true,
}];
const TEMPLATE = `<my-cmp #a></my-cmp>{{ a || a }}`;
expect(tcbWithSpans(TEMPLATE, DIRECTIVES))
.toContain('((_t2 /*23,25*/) || (_t2 /*28,30*/) /*23,30*/);');
});
});
});
});
function tcbWithSpans(template: string, declarations: TestDeclaration[] = []): string {
return tcb(template, declarations, undefined, {emitSpans: true});
}

View File

@ -28,7 +28,8 @@ export type TestPipe = {
export type TestDeclaration = TestDirective | TestPipe; export type TestDeclaration = TestDirective | TestPipe;
export function tcb( export function tcb(
template: string, declarations: TestDeclaration[] = [], config?: TypeCheckingConfig): string { template: string, declarations: TestDeclaration[] = [], config?: TypeCheckingConfig,
options?: {emitSpans?: boolean}): string {
const classes = ['Test', ...declarations.map(decl => decl.name)]; const classes = ['Test', ...declarations.map(decl => decl.name)];
const code = classes.map(name => `class ${name}<T extends string> {}`).join('\n'); const code = classes.map(name => `class ${name}<T extends string> {}`).join('\n');
@ -76,11 +77,15 @@ export function tcb(
checkTemplateBodies: true, checkTemplateBodies: true,
strictSafeNavigationTypes: true, strictSafeNavigationTypes: true,
}; };
options = options || {
emitSpans: false,
};
const tcb = generateTypeCheckBlock( const tcb = generateTypeCheckBlock(
FakeEnvironment.newFake(config), new Reference(clazz), ts.createIdentifier('Test_TCB'), meta); FakeEnvironment.newFake(config), new Reference(clazz), ts.createIdentifier('Test_TCB'), meta);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tcb, sf); const removeComments = !options.emitSpans;
const res = ts.createPrinter({removeComments}).printNode(ts.EmitHint.Unspecified, tcb, sf);
return res.replace(/\s+/g, ' '); return res.replace(/\s+/g, ' ');
} }

View File

@ -7,37 +7,36 @@
*/ */
import {TypeCheckingConfig} from '../src/api'; import {TypeCheckingConfig} from '../src/api';
import {TestDeclaration, TestDirective, tcb} from './test_utils'; import {TestDeclaration, TestDirective, tcb} from './test_utils';
describe('type check blocks', () => { describe('type check blocks', () => {
it('should generate a basic block for a binding', it('should generate a basic block for a binding',
() => { expect(tcb('{{hello}} {{world}}')).toContain('"" + ctx.hello + ctx.world;'); }); () => { expect(tcb('{{hello}} {{world}}')).toContain('"" + (ctx).hello + (ctx).world;'); });
it('should generate literal map expressions', () => { it('should generate literal map expressions', () => {
const TEMPLATE = '{{ method({foo: a, bar: b}) }}'; const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
expect(tcb(TEMPLATE)).toContain('ctx.method({ "foo": ctx.a, "bar": ctx.b });'); expect(tcb(TEMPLATE)).toContain('(ctx).method({ "foo": (ctx).a, "bar": (ctx).b });');
}); });
it('should generate literal array expressions', () => { it('should generate literal array expressions', () => {
const TEMPLATE = '{{ method([a, b]) }}'; const TEMPLATE = '{{ method([a, b]) }}';
expect(tcb(TEMPLATE)).toContain('ctx.method([ctx.a, ctx.b]);'); expect(tcb(TEMPLATE)).toContain('(ctx).method([(ctx).a, (ctx).b]);');
}); });
it('should handle non-null assertions', () => { it('should handle non-null assertions', () => {
const TEMPLATE = `{{a!}}`; const TEMPLATE = `{{a!}}`;
expect(tcb(TEMPLATE)).toContain('(ctx.a!);'); expect(tcb(TEMPLATE)).toContain('(((ctx).a)!);');
}); });
it('should handle keyed property access', () => { it('should handle keyed property access', () => {
const TEMPLATE = `{{a[b]}}`; const TEMPLATE = `{{a[b]}}`;
expect(tcb(TEMPLATE)).toContain('ctx.a[ctx.b];'); expect(tcb(TEMPLATE)).toContain('((ctx).a)[(ctx).b];');
}); });
it('should translate unclaimed bindings to their property equivalent', () => { it('should translate unclaimed bindings to their property equivalent', () => {
const TEMPLATE = `<label [for]="'test'"></label>`; const TEMPLATE = `<label [for]="'test'"></label>`;
expect(tcb(TEMPLATE)).toContain('_t1.htmlFor = "test";'); expect(tcb(TEMPLATE)).toContain('_t1.htmlFor = ("test");');
}); });
it('should handle implicit vars on ng-template', () => { it('should handle implicit vars on ng-template', () => {
@ -55,7 +54,7 @@ describe('type check blocks', () => {
{{ i.value }} {{ i.value }}
<input #i> <input #i>
`; `;
expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); "" + _t1.value;'); expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); "" + (_t1).value;');
}); });
it('should generate a forward directive reference correctly', () => { it('should generate a forward directive reference correctly', () => {
@ -71,7 +70,7 @@ describe('type check blocks', () => {
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t1 = Dir.ngTypeCtor({}); "" + _t1.value; var _t2 = document.createElement("div");'); 'var _t1 = Dir.ngTypeCtor({}); "" + (_t1).value; var _t2 = document.createElement("div");');
}); });
it('should handle style and class bindings specially', () => { it('should handle style and class bindings specially', () => {
@ -79,17 +78,58 @@ describe('type check blocks', () => {
<div [style]="a" [class]="b"></div> <div [style]="a" [class]="b"></div>
`; `;
const block = tcb(TEMPLATE); const block = tcb(TEMPLATE);
expect(block).toContain('ctx.a; ctx.b;'); expect(block).toContain('(ctx).a; (ctx).b;');
// There should be no assignments to the class or style properties. // There should be no assignments to the class or style properties.
expect(block).not.toContain('.class = '); expect(block).not.toContain('.class = ');
expect(block).not.toContain('.style = '); expect(block).not.toContain('.style = ');
}); });
it('should generate a circular directive reference correctly', () => {
const TEMPLATE = `
<div dir #d="dir" [input]="d"></div>
`;
const DIRECTIVES: TestDirective[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
exportAs: ['dir'],
inputs: {input: 'input'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = Dir.ngTypeCtor({ input: (null!) });');
});
it('should generate circular references between two directives correctly', () => {
const TEMPLATE = `
<div #a="dirA" dir-a [inputA]="b">A</div>
<div #b="dirB" dir-b [inputB]="a">B</div>
`;
const DIRECTIVES: TestDirective[] = [
{
type: 'directive',
name: 'DirA',
selector: '[dir-a]',
exportAs: ['dirA'],
inputs: {inputA: 'inputA'},
},
{
type: 'directive',
name: 'DirB',
selector: '[dir-b]',
exportAs: ['dirB'],
inputs: {inputA: 'inputB'},
}
];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t3 = DirB.ngTypeCtor({ inputA: (null!) }); ' +
'var _t2 = DirA.ngTypeCtor({ inputA: (_t3) });');
});
it('should handle $any casts', () => { it('should handle $any casts', () => {
const TEMPLATE = `{{$any(a)}}`; const TEMPLATE = `{{$any(a)}}`;
const block = tcb(TEMPLATE); const block = tcb(TEMPLATE);
expect(block).toContain('(ctx.a as any);'); expect(block).toContain('((ctx).a as any);');
}); });
describe('template guards', () => { describe('template guards', () => {
@ -106,7 +146,7 @@ describe('type check blocks', () => {
}]; }];
const TEMPLATE = `<div *ngIf="person"></div>`; const TEMPLATE = `<div *ngIf="person"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, ctx.person))'); expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, (ctx).person))');
}); });
it('should emit binding guards', () => { it('should emit binding guards', () => {
@ -122,7 +162,7 @@ describe('type check blocks', () => {
}]; }];
const TEMPLATE = `<div *ngIf="person !== null"></div>`; const TEMPLATE = `<div *ngIf="person !== null"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('if (ctx.person !== null)'); expect(block).toContain('if (((ctx).person) !== (null))');
}); });
}); });
@ -164,12 +204,12 @@ describe('type check blocks', () => {
it('should descend into template bodies when enabled', () => { it('should descend into template bodies when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('ctx.a;'); expect(block).toContain('(ctx).a;');
}); });
it('should not descend into template bodies when disabled', () => { it('should not descend into template bodies when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, checkTemplateBodies: false}; const DISABLED_CONFIG = {...BASE_CONFIG, checkTemplateBodies: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).not.toContain('ctx.a;'); expect(block).not.toContain('(ctx).a;');
}); });
}); });
@ -178,14 +218,14 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => { it('should check types of bindings when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: ctx.a })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: ((ctx).a) })');
expect(block).toContain('.nonDirInput = ctx.a;'); expect(block).toContain('.nonDirInput = ((ctx).a);');
}); });
it('should not check types of bindings when disabled', () => { it('should not check types of bindings when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false}; const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: (ctx.a as any) })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: (((ctx).a as any)) })');
expect(block).toContain('.nonDirInput = (ctx.a as any);'); expect(block).toContain('.nonDirInput = (((ctx).a as any));');
}); });
}); });
@ -200,12 +240,12 @@ describe('type check blocks', () => {
it('should check types of pipes when enabled', () => { it('should check types of pipes when enabled', () => {
const block = tcb(TEMPLATE, PIPES); const block = tcb(TEMPLATE, PIPES);
expect(block).toContain('(null as TestPipe).transform(ctx.a, ctx.b, ctx.c);'); expect(block).toContain('(null as TestPipe).transform((ctx).a, (ctx).b, (ctx).c);');
}); });
it('should not check types of pipes when disabled', () => { it('should not check types of pipes when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfPipes: false}; const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfPipes: false};
const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG); const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG);
expect(block).toContain('(null as any).transform(ctx.a, ctx.b, ctx.c);'); expect(block).toContain('(null as any).transform((ctx).a, (ctx).b, (ctx).c);');
}); });
}); });
@ -214,56 +254,15 @@ describe('type check blocks', () => {
it('should use undefined for safe navigation operations when enabled', () => { it('should use undefined for safe navigation operations when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('(ctx.a != null ? ctx.a!.method() : undefined)'); expect(block).toContain('(((ctx).a) != null ? ((ctx).a)!.method() : undefined)');
expect(block).toContain('(ctx.a != null ? ctx.a!.b : undefined)'); expect(block).toContain('(((ctx).a) != null ? ((ctx).a)!.b : undefined)');
}); });
it('should use an \'any\' type for safe navigation operations when disabled', () => { it('should use an \'any\' type for safe navigation operations when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, strictSafeNavigationTypes: false}; const DISABLED_CONFIG = {...BASE_CONFIG, strictSafeNavigationTypes: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('(ctx.a != null ? ctx.a!.method() : null as any)'); expect(block).toContain('(((ctx).a) != null ? ((ctx).a)!.method() : null as any)');
expect(block).toContain('(ctx.a != null ? ctx.a!.b : null as any)'); expect(block).toContain('(((ctx).a) != null ? ((ctx).a)!.b : null as any)');
}); });
}); });
}); });
}); });
it('should generate a circular directive reference correctly', () => {
const TEMPLATE = `
<div dir #d="dir" [input]="d"></div>
`;
const DIRECTIVES: TestDirective[] = [{
type: 'directive',
name: 'Dir',
selector: '[dir]',
exportAs: ['dir'],
inputs: {input: 'input'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = Dir.ngTypeCtor({ input: (null!) });');
});
it('should generate circular references between two directives correctly', () => {
const TEMPLATE = `
<div #a="dirA" dir-a [inputA]="b">A</div>
<div #b="dirB" dir-b [inputB]="a">B</div>
`;
const DIRECTIVES: TestDirective[] = [
{
type: 'directive',
name: 'DirA',
selector: '[dir-a]',
exportAs: ['dirA'],
inputs: {inputA: 'inputA'},
},
{
type: 'directive',
name: 'DirB',
selector: '[dir-b]',
exportAs: ['dirB'],
inputs: {inputA: 'inputB'},
}
];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t3 = DirB.ngTypeCtor({ inputA: (null!) }); ' +
'var _t2 = DirA.ngTypeCtor({ inputA: _t3 });');
});