feat(ivy): add backwards compatibility config to template type-checking (#29698)
View Engine's implementation of naive template type-checking is less advanced than the current Ivy implementation. As a result, Ivy catches lots of typing bugs which VE does not. As a result, it's necessary to tone down the Ivy template type-checker in the default case. This commit introduces a mechanism for doing that, by passing a config to the template type-checking engine. Through this configuration, particular checks can be loosened or disabled entirely. Testing strategy: TCB tests included. PR Close #29698
This commit is contained in:
parent
cd1277cfb7
commit
182e2c7449
|
@ -31,7 +31,7 @@ import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator,
|
||||||
import {ivySwitchTransform} from './switch';
|
import {ivySwitchTransform} from './switch';
|
||||||
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
|
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
|
||||||
import {aliasTransformFactory} from './transform/src/alias';
|
import {aliasTransformFactory} from './transform/src/alias';
|
||||||
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
|
import {TypeCheckContext, TypeCheckProgramHost, TypeCheckingConfig} from './typecheck';
|
||||||
import {normalizeSeparators} from './util/src/path';
|
import {normalizeSeparators} from './util/src/path';
|
||||||
import {getRootDirs, isDtsPath} from './util/src/typescript';
|
import {getRootDirs, isDtsPath} from './util/src/typescript';
|
||||||
|
|
||||||
|
@ -191,7 +191,12 @@ export class NgtscProgram implements api.Program {
|
||||||
const compilation = this.ensureAnalyzed();
|
const compilation = this.ensureAnalyzed();
|
||||||
const diagnostics = [...compilation.diagnostics];
|
const diagnostics = [...compilation.diagnostics];
|
||||||
if (!!this.options.fullTemplateTypeCheck) {
|
if (!!this.options.fullTemplateTypeCheck) {
|
||||||
const ctx = new TypeCheckContext(this.refEmitter !);
|
const config: TypeCheckingConfig = {
|
||||||
|
applyTemplateContextGuards: true,
|
||||||
|
checkTemplateBodies: true,
|
||||||
|
checkTypeOfBindings: true,
|
||||||
|
};
|
||||||
|
const ctx = new TypeCheckContext(config, this.refEmitter !);
|
||||||
compilation.typeCheck(ctx);
|
compilation.typeCheck(ctx);
|
||||||
diagnostics.push(...this.compileTypeCheckProgram(ctx));
|
diagnostics.push(...this.compileTypeCheckProgram(ctx));
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,3 +54,25 @@ export interface TypeCtorMetadata {
|
||||||
*/
|
*/
|
||||||
fields: {inputs: string[]; outputs: string[]; queries: string[];};
|
fields: {inputs: string[]; outputs: string[]; queries: string[];};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TypeCheckingConfig {
|
||||||
|
/**
|
||||||
|
* Whether to check the left-hand side type of binding operations.
|
||||||
|
*
|
||||||
|
* For example, if this is `false` then the expression `[input]="expr"` will have `expr` type-
|
||||||
|
* checked, but not the assignment of the resulting type to the `input` property of whichever
|
||||||
|
* directive or component is receiving the binding. If set to `true`, both sides of the assignment
|
||||||
|
* are checked.
|
||||||
|
*/
|
||||||
|
checkTypeOfBindings: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to narrow the types of template contexts.
|
||||||
|
*/
|
||||||
|
applyTemplateContextGuards: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to descend into template bodies and check any bindings there.
|
||||||
|
*/
|
||||||
|
checkTemplateBodies: boolean;
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {NoopImportRewriter, ReferenceEmitter} from '../../imports';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {ImportManager} from '../../translator';
|
import {ImportManager} from '../../translator';
|
||||||
|
|
||||||
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api';
|
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api';
|
||||||
import {generateTypeCheckBlock} from './type_check_block';
|
import {generateTypeCheckBlock} from './type_check_block';
|
||||||
import {generateTypeCtor} from './type_constructor';
|
import {generateTypeCtor} from './type_constructor';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import {generateTypeCtor} from './type_constructor';
|
||||||
* checking code.
|
* checking code.
|
||||||
*/
|
*/
|
||||||
export class TypeCheckContext {
|
export class TypeCheckContext {
|
||||||
constructor(private refEmitter: ReferenceEmitter) {}
|
constructor(private config: TypeCheckingConfig, private refEmitter: ReferenceEmitter) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `Map` of `ts.SourceFile`s that the context has seen to the operations (additions of methods
|
* A `Map` of `ts.SourceFile`s that the context has seen to the operations (additions of methods
|
||||||
|
@ -141,7 +141,7 @@ export class TypeCheckContext {
|
||||||
this.opMap.set(sf, []);
|
this.opMap.set(sf, []);
|
||||||
}
|
}
|
||||||
const ops = this.opMap.get(sf) !;
|
const ops = this.opMap.get(sf) !;
|
||||||
ops.push(new TcbOp(node, tcbMeta));
|
ops.push(new TcbOp(node, tcbMeta, this.config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,8 +171,8 @@ interface Op {
|
||||||
*/
|
*/
|
||||||
class TcbOp implements Op {
|
class TcbOp implements Op {
|
||||||
constructor(
|
constructor(
|
||||||
readonly node: ClassDeclaration<ts.ClassDeclaration>, readonly meta: TypeCheckBlockMetadata) {
|
readonly node: ClassDeclaration<ts.ClassDeclaration>, readonly meta: TypeCheckBlockMetadata,
|
||||||
}
|
readonly config: TypeCheckingConfig) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type check blocks are inserted immediately after the end of the component class.
|
* Type check blocks are inserted immediately after the end of the component class.
|
||||||
|
@ -181,7 +181,7 @@ class TcbOp implements Op {
|
||||||
|
|
||||||
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
|
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
|
||||||
string {
|
string {
|
||||||
const tcb = generateTypeCheckBlock(this.node, this.meta, im, refEmitter);
|
const tcb = generateTypeCheckBlock(this.node, this.meta, this.config, im, refEmitter);
|
||||||
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
|
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import {AST, ASTWithSource, Binary, Conditional, Interpolation, KeyedRead, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PropertyRead} from '@angular/compiler';
|
import {AST, ASTWithSource, Binary, Conditional, Interpolation, KeyedRead, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PropertyRead} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {TypeCheckingConfig} from './api';
|
||||||
const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
||||||
['+', ts.SyntaxKind.PlusToken],
|
['+', ts.SyntaxKind.PlusToken],
|
||||||
['-', ts.SyntaxKind.MinusToken],
|
['-', ts.SyntaxKind.MinusToken],
|
||||||
|
@ -34,7 +35,8 @@ const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
||||||
* AST.
|
* AST.
|
||||||
*/
|
*/
|
||||||
export function astToTypescript(
|
export function astToTypescript(
|
||||||
ast: AST, maybeResolve: (ast: AST) => ts.Expression | null): ts.Expression {
|
ast: AST, maybeResolve: (ast: AST) => ts.Expression | null,
|
||||||
|
config: TypeCheckingConfig): ts.Expression {
|
||||||
const resolved = maybeResolve(ast);
|
const resolved = maybeResolve(ast);
|
||||||
if (resolved !== null) {
|
if (resolved !== null) {
|
||||||
return resolved;
|
return resolved;
|
||||||
|
@ -42,17 +44,17 @@ export function astToTypescript(
|
||||||
// Branch based on the type of expression being processed.
|
// Branch based on the type of expression being processed.
|
||||||
if (ast instanceof ASTWithSource) {
|
if (ast instanceof ASTWithSource) {
|
||||||
// Fall through to the underlying AST.
|
// Fall through to the underlying AST.
|
||||||
return astToTypescript(ast.ast, maybeResolve);
|
return astToTypescript(ast.ast, maybeResolve, config);
|
||||||
} else if (ast instanceof PropertyRead) {
|
} else if (ast instanceof PropertyRead) {
|
||||||
// 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 = astToTypescript(ast.receiver, maybeResolve);
|
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||||
return ts.createPropertyAccess(receiver, ast.name);
|
return ts.createPropertyAccess(receiver, ast.name);
|
||||||
} else if (ast instanceof Interpolation) {
|
} else if (ast instanceof Interpolation) {
|
||||||
return astArrayToExpression(ast.expressions, maybeResolve);
|
return astArrayToExpression(ast.expressions, maybeResolve, config);
|
||||||
} else if (ast instanceof Binary) {
|
} else if (ast instanceof Binary) {
|
||||||
const lhs = astToTypescript(ast.left, maybeResolve);
|
const lhs = astToTypescript(ast.left, maybeResolve, config);
|
||||||
const rhs = astToTypescript(ast.right, maybeResolve);
|
const rhs = astToTypescript(ast.right, maybeResolve, config);
|
||||||
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}`);
|
||||||
|
@ -67,30 +69,30 @@ export function astToTypescript(
|
||||||
return ts.createLiteral(ast.value);
|
return ts.createLiteral(ast.value);
|
||||||
}
|
}
|
||||||
} else if (ast instanceof MethodCall) {
|
} else if (ast instanceof MethodCall) {
|
||||||
const receiver = astToTypescript(ast.receiver, maybeResolve);
|
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||||
const method = ts.createPropertyAccess(receiver, ast.name);
|
const method = ts.createPropertyAccess(receiver, ast.name);
|
||||||
const args = ast.args.map(expr => astToTypescript(expr, maybeResolve));
|
const args = ast.args.map(expr => astToTypescript(expr, maybeResolve, config));
|
||||||
return ts.createCall(method, undefined, args);
|
return ts.createCall(method, undefined, args);
|
||||||
} else if (ast instanceof Conditional) {
|
} else if (ast instanceof Conditional) {
|
||||||
const condExpr = astToTypescript(ast.condition, maybeResolve);
|
const condExpr = astToTypescript(ast.condition, maybeResolve, config);
|
||||||
const trueExpr = astToTypescript(ast.trueExp, maybeResolve);
|
const trueExpr = astToTypescript(ast.trueExp, maybeResolve, config);
|
||||||
const falseExpr = astToTypescript(ast.falseExp, maybeResolve);
|
const falseExpr = astToTypescript(ast.falseExp, maybeResolve, config);
|
||||||
return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
|
return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
|
||||||
} else if (ast instanceof LiteralArray) {
|
} else if (ast instanceof LiteralArray) {
|
||||||
const elements = ast.expressions.map(expr => astToTypescript(expr, maybeResolve));
|
const elements = ast.expressions.map(expr => astToTypescript(expr, maybeResolve, config));
|
||||||
return ts.createArrayLiteral(elements);
|
return ts.createArrayLiteral(elements);
|
||||||
} else if (ast instanceof LiteralMap) {
|
} else if (ast instanceof LiteralMap) {
|
||||||
const properties = ast.keys.map(({key}, idx) => {
|
const properties = ast.keys.map(({key}, idx) => {
|
||||||
const value = astToTypescript(ast.values[idx], maybeResolve);
|
const value = astToTypescript(ast.values[idx], maybeResolve, config);
|
||||||
return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
|
return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
|
||||||
});
|
});
|
||||||
return ts.createObjectLiteral(properties, true);
|
return ts.createObjectLiteral(properties, true);
|
||||||
} else if (ast instanceof KeyedRead) {
|
} else if (ast instanceof KeyedRead) {
|
||||||
const receiver = astToTypescript(ast.obj, maybeResolve);
|
const receiver = astToTypescript(ast.obj, maybeResolve, config);
|
||||||
const key = astToTypescript(ast.key, maybeResolve);
|
const key = astToTypescript(ast.key, maybeResolve, config);
|
||||||
return ts.createElementAccess(receiver, key);
|
return ts.createElementAccess(receiver, key);
|
||||||
} else if (ast instanceof NonNullAssert) {
|
} else if (ast instanceof NonNullAssert) {
|
||||||
const expr = astToTypescript(ast.expression, maybeResolve);
|
const expr = astToTypescript(ast.expression, maybeResolve, config);
|
||||||
return ts.createNonNullExpression(expr);
|
return ts.createNonNullExpression(expr);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown node type: ${Object.getPrototypeOf(ast).constructor}`);
|
throw new Error(`Unknown node type: ${Object.getPrototypeOf(ast).constructor}`);
|
||||||
|
@ -102,13 +104,14 @@ export function astToTypescript(
|
||||||
* and separating them with commas.
|
* and separating them with commas.
|
||||||
*/
|
*/
|
||||||
function astArrayToExpression(
|
function astArrayToExpression(
|
||||||
astArray: AST[], maybeResolve: (ast: AST) => ts.Expression | null): ts.Expression {
|
astArray: AST[], maybeResolve: (ast: AST) => ts.Expression | null,
|
||||||
|
config: TypeCheckingConfig): ts.Expression {
|
||||||
// Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a
|
// Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a
|
||||||
// `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as
|
// `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as
|
||||||
// it will be modified during the reduction.
|
// it will be modified during the reduction.
|
||||||
const asts = astArray.slice();
|
const asts = astArray.slice();
|
||||||
return asts.reduce(
|
return asts.reduce(
|
||||||
(lhs, ast) =>
|
(lhs, ast) => ts.createBinary(
|
||||||
ts.createBinary(lhs, ts.SyntaxKind.CommaToken, astToTypescript(ast, maybeResolve)),
|
lhs, ts.SyntaxKind.CommaToken, astToTypescript(ast, maybeResolve, config)),
|
||||||
astToTypescript(asts.pop() !, maybeResolve));
|
astToTypescript(asts.pop() !, maybeResolve, config));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../i
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {ImportManager, translateExpression, translateType} from '../../translator';
|
import {ImportManager, translateExpression, translateType} from '../../translator';
|
||||||
|
|
||||||
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
|
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from './api';
|
||||||
import {astToTypescript} from './expression';
|
import {astToTypescript} from './expression';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,8 +29,10 @@ import {astToTypescript} from './expression';
|
||||||
*/
|
*/
|
||||||
export function generateTypeCheckBlock(
|
export function generateTypeCheckBlock(
|
||||||
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCheckBlockMetadata,
|
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCheckBlockMetadata,
|
||||||
importManager: ImportManager, refEmitter: ReferenceEmitter): ts.FunctionDeclaration {
|
config: TypeCheckingConfig, importManager: ImportManager,
|
||||||
const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
|
refEmitter: ReferenceEmitter): ts.FunctionDeclaration {
|
||||||
|
const tcb =
|
||||||
|
new Context(config, meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
|
||||||
const scope = Scope.forNodes(tcb, null, tcb.boundTarget.target.template !);
|
const scope = Scope.forNodes(tcb, null, tcb.boundTarget.target.template !);
|
||||||
|
|
||||||
return ts.createFunctionDeclaration(
|
return ts.createFunctionDeclaration(
|
||||||
|
@ -187,7 +189,7 @@ class TcbTemplateBodyOp extends TcbOp {
|
||||||
|
|
||||||
// The second kind of guard is a template context guard. This guard narrows the template
|
// The second kind of guard is a template context guard. This guard narrows the template
|
||||||
// rendering context variable `ctx`.
|
// rendering context variable `ctx`.
|
||||||
if (dir.hasNgTemplateContextGuard) {
|
if (dir.hasNgTemplateContextGuard && this.tcb.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]);
|
||||||
directiveGuards.push(guardInvoke);
|
directiveGuards.push(guardInvoke);
|
||||||
|
@ -288,7 +290,13 @@ class TcbUnclaimedInputsOp extends TcbOp {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expr = tcbExpression(binding.value, this.tcb, this.scope);
|
let expr = tcbExpression(binding.value, this.tcb, this.scope);
|
||||||
|
|
||||||
|
// If checking the type of bindings is disabled, cast the resulting expression to 'any' before
|
||||||
|
// the assignment.
|
||||||
|
if (!this.tcb.config.checkTypeOfBindings) {
|
||||||
|
expr = tsCastToAny(expr);
|
||||||
|
}
|
||||||
|
|
||||||
if (binding.type === BindingType.Property) {
|
if (binding.type === BindingType.Property) {
|
||||||
if (binding.name !== 'style' && binding.name !== 'class') {
|
if (binding.name !== 'style' && binding.name !== 'class') {
|
||||||
|
@ -331,6 +339,7 @@ class Context {
|
||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
readonly config: TypeCheckingConfig,
|
||||||
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
|
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
|
||||||
private sourceFile: ts.SourceFile, private importManager: ImportManager,
|
private sourceFile: ts.SourceFile, private importManager: ImportManager,
|
||||||
private refEmitter: ReferenceEmitter) {}
|
private refEmitter: ReferenceEmitter) {}
|
||||||
|
@ -605,9 +614,11 @@ class Scope {
|
||||||
} else if (node instanceof TmplAstTemplate) {
|
} else if (node instanceof TmplAstTemplate) {
|
||||||
// Template children are rendered in a child scope.
|
// Template children are rendered in a child scope.
|
||||||
this.appendDirectivesAndInputsOfNode(node);
|
this.appendDirectivesAndInputsOfNode(node);
|
||||||
const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
|
if (this.tcb.config.checkTemplateBodies) {
|
||||||
this.templateCtxOpMap.set(node, ctxIndex);
|
const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
|
||||||
this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node));
|
this.templateCtxOpMap.set(node, ctxIndex);
|
||||||
|
this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node));
|
||||||
|
}
|
||||||
} else if (node instanceof TmplAstBoundText) {
|
} else if (node instanceof TmplAstBoundText) {
|
||||||
this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node));
|
this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node));
|
||||||
}
|
}
|
||||||
|
@ -681,7 +692,7 @@ function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
|
||||||
// `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));
|
return astToTypescript(ast, (ast) => tcbResolve(ast, tcb, scope), tcb.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -695,7 +706,12 @@ 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(b => ts.createPropertyAssignment(b.field, b.expression));
|
const members = bindings.map(({field, expression}) => {
|
||||||
|
if (!tcb.config.checkTypeOfBindings) {
|
||||||
|
expression = tsCastToAny(expression);
|
||||||
|
}
|
||||||
|
return ts.createPropertyAssignment(field, expression);
|
||||||
|
});
|
||||||
|
|
||||||
// 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
|
||||||
// from the matched inputs.
|
// from the matched inputs.
|
||||||
|
@ -874,3 +890,8 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tsCastToAny(expr: ts.Expression): ts.Expression {
|
||||||
|
return ts.createParen(
|
||||||
|
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
||||||
import {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports';
|
import {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports';
|
||||||
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
|
||||||
import {ImportManager} from '../../translator';
|
import {ImportManager} from '../../translator';
|
||||||
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from '../src/api';
|
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../src/api';
|
||||||
import {generateTypeCheckBlock} from '../src/type_check_block';
|
import {generateTypeCheckBlock} from '../src/type_check_block';
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,6 +74,66 @@ describe('type check blocks', () => {
|
||||||
expect(block).not.toContain('.class = ');
|
expect(block).not.toContain('.class = ');
|
||||||
expect(block).not.toContain('.style = ');
|
expect(block).not.toContain('.style = ');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('config', () => {
|
||||||
|
const DIRECTIVES: TestDirective[] = [{
|
||||||
|
name: 'Dir',
|
||||||
|
selector: '[dir]',
|
||||||
|
exportAs: ['dir'],
|
||||||
|
inputs: {'dirInput': 'dirInput'},
|
||||||
|
hasNgTemplateContextGuard: true,
|
||||||
|
}];
|
||||||
|
const BASE_CONFIG: TypeCheckingConfig = {
|
||||||
|
applyTemplateContextGuards: true,
|
||||||
|
checkTemplateBodies: true,
|
||||||
|
checkTypeOfBindings: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('config.applyTemplateContextGuards', () => {
|
||||||
|
const TEMPLATE = `<div *dir></div>`;
|
||||||
|
const GUARD_APPLIED = 'if (i0.Dir.ngTemplateContextGuard(';
|
||||||
|
|
||||||
|
it('should apply template context guards when enabled', () => {
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
|
expect(block).toContain(GUARD_APPLIED);
|
||||||
|
});
|
||||||
|
it('should not apply template context guards when disabled', () => {
|
||||||
|
const DISABLED_CONFIG = {...BASE_CONFIG, applyTemplateContextGuards: false};
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||||
|
expect(block).not.toContain(GUARD_APPLIED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config.checkTemplateBodies', () => {
|
||||||
|
const TEMPLATE = `<ng-template>{{a}}</ng-template>`;
|
||||||
|
|
||||||
|
it('should descend into template bodies when enabled', () => {
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
|
expect(block).toContain('ctx.a;');
|
||||||
|
});
|
||||||
|
it('should not descend into template bodies when disabled', () => {
|
||||||
|
const DISABLED_CONFIG = {...BASE_CONFIG, checkTemplateBodies: false};
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||||
|
expect(block).not.toContain('ctx.a;');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config.checkTypeOfBindings', () => {
|
||||||
|
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="a"></div>`;
|
||||||
|
|
||||||
|
it('should check types of bindings when enabled', () => {
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
|
expect(block).toContain('i0.Dir.ngTypeCtor({ dirInput: ctx.a })');
|
||||||
|
expect(block).toContain('.nonDirInput = ctx.a;');
|
||||||
|
});
|
||||||
|
it('should not check types of bindings when disabled', () => {
|
||||||
|
const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false};
|
||||||
|
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||||
|
expect(block).toContain('i0.Dir.ngTypeCtor({ dirInput: (ctx.a as any) })');
|
||||||
|
expect(block).toContain('.nonDirInput = (ctx.a as any);');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a circular directive reference correctly', () => {
|
it('should generate a circular directive reference correctly', () => {
|
||||||
|
@ -128,7 +188,8 @@ type TestDirective =
|
||||||
Partial<Pick<TypeCheckableDirectiveMeta, Exclude<keyof TypeCheckableDirectiveMeta, 'ref'>>>&
|
Partial<Pick<TypeCheckableDirectiveMeta, Exclude<keyof TypeCheckableDirectiveMeta, 'ref'>>>&
|
||||||
{selector: string, name: string};
|
{selector: string, name: string};
|
||||||
|
|
||||||
function tcb(template: string, directives: TestDirective[] = []): string {
|
function tcb(
|
||||||
|
template: string, directives: TestDirective[] = [], config?: TypeCheckingConfig): string {
|
||||||
const classes = ['Test', ...directives.map(dir => dir.name)];
|
const classes = ['Test', ...directives.map(dir => dir.name)];
|
||||||
const code = classes.map(name => `class ${name} {}`).join('\n');
|
const code = classes.map(name => `class ${name} {}`).join('\n');
|
||||||
|
|
||||||
|
@ -161,9 +222,15 @@ function tcb(template: string, directives: TestDirective[] = []): string {
|
||||||
fnName: 'Test_TCB',
|
fnName: 'Test_TCB',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config = config || {
|
||||||
|
applyTemplateContextGuards: true,
|
||||||
|
checkTypeOfBindings: true,
|
||||||
|
checkTemplateBodies: true,
|
||||||
|
};
|
||||||
|
|
||||||
const im = new ImportManager(undefined, 'i');
|
const im = new ImportManager(undefined, 'i');
|
||||||
const tcb =
|
const tcb = generateTypeCheckBlock(
|
||||||
generateTypeCheckBlock(clazz, meta, im, new ReferenceEmitter([new FakeReferenceStrategy()]));
|
clazz, meta, config, im, new ReferenceEmitter([new FakeReferenceStrategy()]));
|
||||||
|
|
||||||
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tcb, sf);
|
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tcb, sf);
|
||||||
return res.replace(/\s+/g, ' ');
|
return res.replace(/\s+/g, ' ');
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {LogicalFileSystem} from '../../path';
|
||||||
import {isNamedClassDeclaration} from '../../reflection';
|
import {isNamedClassDeclaration} from '../../reflection';
|
||||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||||
import {getRootDirs} from '../../util/src/typescript';
|
import {getRootDirs} from '../../util/src/typescript';
|
||||||
|
import {TypeCheckingConfig} from '../src/api';
|
||||||
import {TypeCheckContext} from '../src/context';
|
import {TypeCheckContext} from '../src/context';
|
||||||
import {TypeCheckProgramHost} from '../src/host';
|
import {TypeCheckProgramHost} from '../src/host';
|
||||||
|
|
||||||
|
@ -24,6 +25,12 @@ const LIB_D_TS = {
|
||||||
type NonNullable<T> = T extends null | undefined ? never : T;`
|
type NonNullable<T> = T extends null | undefined ? never : T;`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
||||||
|
applyTemplateContextGuards: true,
|
||||||
|
checkTemplateBodies: true,
|
||||||
|
checkTypeOfBindings: true,
|
||||||
|
};
|
||||||
|
|
||||||
describe('ngtsc typechecking', () => {
|
describe('ngtsc typechecking', () => {
|
||||||
describe('ctors', () => {
|
describe('ctors', () => {
|
||||||
it('compiles a basic type constructor', () => {
|
it('compiles a basic type constructor', () => {
|
||||||
|
@ -47,7 +54,7 @@ TestClass.ngTypeCtor({value: 'test'});
|
||||||
new AbsoluteModuleStrategy(program, checker, options, host),
|
new AbsoluteModuleStrategy(program, checker, options, host),
|
||||||
new LogicalProjectStrategy(checker, logicalFs),
|
new LogicalProjectStrategy(checker, logicalFs),
|
||||||
]);
|
]);
|
||||||
const ctx = new TypeCheckContext(emitter);
|
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter);
|
||||||
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration);
|
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration);
|
||||||
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, {
|
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, {
|
||||||
fnName: 'ngTypeCtor',
|
fnName: 'ngTypeCtor',
|
||||||
|
|
Loading…
Reference in New Issue