perf(compiler-cli): only generate type-check code for referenced DOM elements (#38418)

The template type-checker would generate a statement with a call
expression for all DOM elements in a template of the form:

```
const _t1 = document.createElement("div");
```

Profiling has shown that this is a particularly expensive call to
perform type inference on, as TypeScript needs to perform signature
selection of `Document.createElement` and resolve the exact type from
the `HTMLElementTagNameMap`. However, it can be observed that the
statement by itself does not contribute anything to the type-checking
result if `_t1` is not actually used anywhere, which is only rarely the
case---it requires that the element is referenced by its name from
somewhere else in the template. Consequently, the type-checker can skip
generating this statement altogether for most DOM elements.

The effect of this optimization is significant in several phases:
1. Less type-check code to generate
2. Less type-check code to emit and parse again
3. No expensive type inference to perform for the call expression

The effect on phase 3 is the most significant here, as type-checking is
not currently incremental in the sense that only phases 1 and 2 can
be reused from a prior compilation. The actual type-checking of all
templates in phase 3 needs to be repeated on each incremental
compilation, so any performance gains we achieve here are very
beneficial.

PR Close #38418
This commit is contained in:
JoostK 2020-08-11 22:29:34 +02:00 committed by Andrew Scott
parent 175c79d1d8
commit f42e6ce917
4 changed files with 166 additions and 86 deletions

View File

@ -99,6 +99,13 @@ export function generateTypeCheckBlock(
* `ts.Expression` which can be used to reference the operation's result.
*/
abstract class TcbOp {
/**
* Set to true if this operation can be considered optional. Optional operations are only executed
* when depended upon by other operations, otherwise they are disregarded. This allows for less
* code to generate, parse and type-check, overall positively contributing to performance.
*/
abstract readonly optional: boolean;
abstract execute(): ts.Expression|null;
/**
@ -125,6 +132,13 @@ class TcbElementOp extends TcbOp {
super();
}
get optional() {
// The statement generated by this operation is only used for type-inference of the DOM
// element's type and won't report diagnostics by itself, so the operation is marked as optional
// to avoid generating statements for DOM elements that are never referenced.
return true;
}
execute(): ts.Identifier {
const id = this.tcb.allocateId();
// Add the declaration of the element using document.createElement.
@ -148,6 +162,10 @@ class TcbVariableOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Identifier {
// Look for a context variable for the template.
const ctx = this.scope.resolve(this.template);
@ -176,6 +194,10 @@ class TcbTemplateContextOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Identifier {
// Allocate a template ctx variable and declare it with an 'any' type. The type of this variable
// may be narrowed as a result of template guard conditions.
@ -198,6 +220,10 @@ class TcbTemplateBodyOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
// An `if` will be constructed, within which the template's children will be type checked. The
// `if` is used for two reasons: it creates a new syntactic scope, isolating variables declared
@ -301,6 +327,10 @@ class TcbTextInterpolationOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
const expr = tcbExpression(this.binding.value, this.tcb, this.scope);
this.scope.addStatement(ts.createExpressionStatement(expr));
@ -324,6 +354,10 @@ class TcbDirectiveTypeOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Identifier {
const id = this.tcb.allocateId();
@ -352,6 +386,10 @@ class TcbDirectiveCtorOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Identifier {
const id = this.tcb.allocateId();
@ -409,6 +447,10 @@ class TcbDirectiveInputsOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
const dirId = this.scope.resolve(this.node, this.dir);
@ -514,6 +556,10 @@ class TcbDirectiveCtorCircularFallbackOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Identifier {
const id = this.tcb.allocateId();
const typeCtor = this.tcb.env.typeCtorFor(this.dir);
@ -541,6 +587,10 @@ class TcbDomSchemaCheckerOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): ts.Expression|null {
if (this.checkElement) {
this.tcb.domSchemaChecker.checkElement(this.tcb.id, this.element, this.tcb.schemas);
@ -597,10 +647,14 @@ class TcbUnclaimedInputsOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
// `this.inputs` contains only those bindings not matched by any directive. These bindings go to
// the element itself.
const elId = this.scope.resolve(this.element);
let elId: ts.Expression|null = null;
// TODO(alxhub): this could be more efficient.
for (const binding of this.element.inputs) {
@ -622,6 +676,9 @@ class TcbUnclaimedInputsOp extends TcbOp {
if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
if (binding.name !== 'style' && binding.name !== 'class') {
if (elId === null) {
elId = this.scope.resolve(this.element);
}
// A direct binding to a property.
const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
const prop = ts.createElementAccess(elId, ts.createStringLiteral(propertyName));
@ -656,6 +713,10 @@ class TcbDirectiveOutputsOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
const dirId = this.scope.resolve(this.node, this.dir);
@ -723,8 +784,12 @@ class TcbUnclaimedOutputsOp extends TcbOp {
super();
}
get optional() {
return false;
}
execute(): null {
const elId = this.scope.resolve(this.element);
let elId: ts.Expression|null = null;
// TODO(alxhub): this could be more efficient.
for (const output of this.element.outputs) {
@ -749,6 +814,9 @@ class TcbUnclaimedOutputsOp extends TcbOp {
// base `Event` type.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
if (elId === null) {
elId = this.scope.resolve(this.element);
}
const call = ts.createCall(
/* expression */ ts.createPropertyAccess(elId, 'addEventListener'),
/* typeArguments */ undefined,
@ -965,7 +1033,7 @@ class Scope {
*/
render(): ts.Statement[] {
for (let i = 0; i < this.opQueue.length; i++) {
this.executeOp(i);
this.executeOp(i, /* skipOptional */ true);
}
return this.statements;
}
@ -1031,7 +1099,7 @@ class Scope {
* Like `executeOp`, but assert that the operation actually returned `ts.Expression`.
*/
private resolveOp(opIndex: number): ts.Expression {
const res = this.executeOp(opIndex);
const res = this.executeOp(opIndex, /* skipOptional */ false);
if (res === null) {
throw new Error(`Error resolving operation, got null`);
}
@ -1045,12 +1113,16 @@ class Scope {
* and also protects against a circular dependency from the operation to itself by temporarily
* setting the operation's result to a special expression.
*/
private executeOp(opIndex: number): ts.Expression|null {
private executeOp(opIndex: number, skipOptional: boolean): ts.Expression|null {
const op = this.opQueue[opIndex];
if (!(op instanceof TcbOp)) {
return op;
}
if (skipOptional && op.optional) {
return null;
}
// Set the result of the operation in the queue to its circular fallback. If executing this
// operation results in a circular dependency, this will prevent an infinite loop and allow for
// the resolution of such cycles.

View File

@ -158,7 +158,7 @@ describe('type check blocks diagnostics', () => {
}];
const TEMPLATE = `<my-cmp #a></my-cmp>{{ a || a }}`;
expect(tcbWithSpans(TEMPLATE, DIRECTIVES))
.toContain('((_t2 /*23,24*/) || (_t2 /*28,29*/) /*23,29*/);');
.toContain('((_t1 /*23,24*/) || (_t1 /*28,29*/) /*23,29*/);');
});
});
});

View File

@ -55,7 +55,7 @@ describe('type check blocks', () => {
selector: '[dir]',
inputs: {inputA: 'inputA'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2: DirA = (null!); _t2.inputA = ("value");');
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1: DirA = (null!); _t1.inputA = ("value");');
});
it('should handle multiple bindings to the same property', () => {
@ -67,8 +67,8 @@ describe('type check blocks', () => {
inputs: {inputA: 'inputA'},
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2.inputA = (1);');
expect(block).toContain('_t2.inputA = (2);');
expect(block).toContain('_t1.inputA = (1);');
expect(block).toContain('_t1.inputA = (2);');
});
it('should handle empty bindings', () => {
@ -79,7 +79,7 @@ describe('type check blocks', () => {
selector: '[dir-a]',
inputs: {inputA: 'inputA'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2.inputA = (undefined);');
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);');
});
it('should handle bindings without value', () => {
@ -90,7 +90,7 @@ describe('type check blocks', () => {
selector: '[dir-a]',
inputs: {inputA: 'inputA'},
}];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t2.inputA = (undefined);');
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);');
});
it('should handle implicit vars on ng-template', () => {
@ -124,7 +124,7 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)), "fieldB": (null as any) });');
'var _t1 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)), "fieldB": (null as any) });');
});
it('should handle multiple bindings to the same property', () => {
@ -157,7 +157,7 @@ describe('type check blocks', () => {
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'var _t2 = Dir.ngTypeCtor({ "color": (null as any), "strong": (null as any), "enabled": (null as any) });');
'var _t1 = Dir.ngTypeCtor({ "color": (null as any), "strong": (null as any), "enabled": (null as any) });');
expect(block).toContain('"blue"; false; true;');
});
@ -175,8 +175,8 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t3 = Dir.ngTypeCtor((null!)); ' +
'var _t2 = Dir.ngTypeCtor({ "input": (_t3) });');
'var _t2 = Dir.ngTypeCtor((null!)); ' +
'var _t1 = Dir.ngTypeCtor({ "input": (_t2) });');
});
it('should generate circular references between two directives correctly', () => {
@ -204,9 +204,9 @@ describe('type check blocks', () => {
];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t4 = DirA.ngTypeCtor((null!)); ' +
'var _t3 = DirB.ngTypeCtor({ "inputB": (_t4) }); ' +
'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) });');
'var _t3 = DirA.ngTypeCtor((null!)); ' +
'var _t2 = DirB.ngTypeCtor({ "inputB": (_t3) }); ' +
'var _t1 = DirA.ngTypeCtor({ "inputA": (_t2) });');
});
it('should handle empty bindings', () => {
@ -247,12 +247,23 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)) }); ' +
'var _t3: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t3 = (((ctx).foo));');
'var _t1 = Dir.ngTypeCtor({ "fieldA": (((ctx).foo)) }); ' +
'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t2 = (((ctx).foo));');
});
});
it('should only generate code for DOM elements that are actually referenced', () => {
const TEMPLATE = `
<div></div>
<button #me (click)="handle(me)"></button>
`;
const block = tcb(TEMPLATE);
expect(block).not.toContain('"div"');
expect(block).toContain('var _t1 = document.createElement("button");');
expect(block).toContain('(ctx).handle(_t1);');
});
it('should generate a forward element reference correctly', () => {
const TEMPLATE = `
{{ i.value }}
@ -273,9 +284,7 @@ describe('type check blocks', () => {
selector: '[dir]',
exportAs: ['dir'],
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t1: Dir = (null!); "" + ((_t1).value); var _t2 = document.createElement("div");');
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t1: Dir = (null!); "" + ((_t1).value);');
});
it('should handle style and class bindings specially', () => {
@ -301,7 +310,7 @@ describe('type check blocks', () => {
inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'},
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('var _t2: Dir = (null!);');
expect(block).toContain('var _t1: Dir = (null!);');
expect(block).not.toContain('"color"');
expect(block).not.toContain('"strong"');
expect(block).not.toContain('"enabled"');
@ -321,8 +330,8 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'_t2.input = (_t2);');
'var _t1: Dir = (null!); ' +
'_t1.input = (_t1);');
});
it('should generate circular references between two directives correctly', () => {
@ -348,11 +357,10 @@ describe('type check blocks', () => {
];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: DirA = (null!); ' +
'var _t3: DirB = (null!); ' +
'_t2.inputA = (_t3); ' +
'var _t4 = document.createElement("div"); ' +
'_t3.inputA = (_t2);');
'var _t1: DirA = (null!); ' +
'var _t2: DirB = (null!); ' +
'_t1.inputA = (_t2); ' +
'_t2.inputA = (_t1);');
});
it('should handle undeclared properties', () => {
@ -368,7 +376,7 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'var _t1: Dir = (null!); ' +
'(((ctx).foo)); ');
});
@ -385,9 +393,9 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'var _t3: typeof _t2["fieldA"] = (null!); ' +
'_t3 = (((ctx).foo)); ');
'var _t1: Dir = (null!); ' +
'var _t2: typeof _t1["fieldA"] = (null!); ' +
'_t2 = (((ctx).foo)); ');
});
it('should assign properties via element access for field names that are not JS identifiers',
@ -404,8 +412,8 @@ describe('type check blocks', () => {
}];
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2["some-input.xs"] = (((ctx).foo)); ');
'var _t1: Dir = (null!); ' +
'_t1["some-input.xs"] = (((ctx).foo)); ');
});
it('should handle a single property bound to multiple fields', () => {
@ -421,8 +429,8 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'_t2.field2 = _t2.field1 = (((ctx).foo));');
'var _t1: Dir = (null!); ' +
'_t1.field2 = _t1.field1 = (((ctx).foo));');
});
it('should handle a single property bound to multiple fields, where one of them is coerced',
@ -440,9 +448,9 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'var _t3: typeof Dir.ngAcceptInputType_field1 = (null!); ' +
'_t2.field2 = _t3 = (((ctx).foo));');
'var _t1: Dir = (null!); ' +
'var _t2: typeof Dir.ngAcceptInputType_field1 = (null!); ' +
'_t1.field2 = _t2 = (((ctx).foo));');
});
it('should handle a single property bound to multiple fields, where one of them is undeclared',
@ -460,8 +468,8 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'_t2.field2 = (((ctx).foo));');
'var _t1: Dir = (null!); ' +
'_t1.field2 = (((ctx).foo));');
});
it('should use coercion types if declared', () => {
@ -477,9 +485,9 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'var _t3: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t3 = (((ctx).foo));');
'var _t1: Dir = (null!); ' +
'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t2 = (((ctx).foo));');
});
it('should use coercion types if declared, even when backing field is not declared', () => {
@ -496,9 +504,9 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t2: Dir = (null!); ' +
'var _t3: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t3 = (((ctx).foo));');
'var _t1: Dir = (null!); ' +
'var _t2: typeof Dir.ngAcceptInputType_fieldA = (null!); ' +
'_t2 = (((ctx).foo));');
});
it('should handle $any casts', () => {
@ -561,7 +569,7 @@ describe('type check blocks', () => {
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
});
it('should emit a listener function with AnimationEvent for animation events', () => {
@ -658,14 +666,14 @@ describe('type check blocks', () => {
it('should include null and undefined when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2.dirInput = (((ctx).a));');
expect(block).toContain('_t1.dirInput = (((ctx).a));');
expect(block).toContain('((ctx).b);');
});
it('should use the non-null assertion operator when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2.dirInput = (((ctx).a)!);');
expect(block).toContain('_t1.dirInput = (((ctx).a)!);');
expect(block).toContain('((ctx).b)!;');
});
});
@ -674,7 +682,7 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2.dirInput = (((ctx).a));');
expect(block).toContain('_t1.dirInput = (((ctx).a));');
expect(block).toContain('((ctx).b);');
});
@ -683,7 +691,7 @@ describe('type check blocks', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2.dirInput = ((((ctx).a) as any));');
expect(block).toContain('_t1.dirInput = ((((ctx).a) as any));');
expect(block).toContain('(((ctx).b) as any);');
});
@ -692,7 +700,7 @@ describe('type check blocks', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t2.dirInput = ((((((ctx).a)) === (((ctx).b))) as any));');
expect(block).toContain('_t1.dirInput = ((((((ctx).a)) === (((ctx).b))) as any));');
});
});
@ -702,9 +710,9 @@ describe('type check blocks', () => {
it('should check types of directive outputs when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain(
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
});
it('should not check types of directive outputs when disabled', () => {
const DISABLED_CONFIG:
@ -713,7 +721,7 @@ describe('type check blocks', () => {
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
// Note that DOM events are still checked, that is controlled by `checkTypeOfDomEvents`
expect(block).toContain(
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
});
});
@ -739,9 +747,9 @@ describe('type check blocks', () => {
it('should check types of DOM events when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain(
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
});
it('should not check types of DOM events when disabled', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomEvents: false};
@ -749,7 +757,7 @@ describe('type check blocks', () => {
// Note that directive outputs are still checked, that is controlled by
// `checkTypeOfOutputEvents`
expect(block).toContain(
'_outputHelper(_t2["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
});
});
@ -785,7 +793,7 @@ describe('type check blocks', () => {
it('should trace references to a directive when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('(_t2).value');
expect(block).toContain('(_t1).value');
});
it('should trace references to an <ng-template> when enabled', () => {
@ -812,9 +820,9 @@ describe('type check blocks', () => {
it('should assign string value to the input when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('_t2.disabled = ("");');
expect(block).toContain('_t2.cols = ("3");');
expect(block).toContain('_t2.rows = (2);');
expect(block).toContain('_t1.disabled = ("");');
expect(block).toContain('_t1.cols = ("3");');
expect(block).toContain('_t1.rows = (2);');
});
it('should use any for attributes but still check bound attributes when disabled', () => {
@ -822,7 +830,7 @@ describe('type check blocks', () => {
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).not.toContain('"disabled"');
expect(block).not.toContain('"cols"');
expect(block).toContain('_t2.rows = (2);');
expect(block).toContain('_t1.rows = (2);');
});
});
@ -912,8 +920,8 @@ describe('type check blocks', () => {
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2["some-input.xs"] = (((ctx).foo)); ');
'var _t1: Dir = (null!); ' +
'_t1["some-input.xs"] = (((ctx).foo)); ');
});
it('should assign restricted properties via property access', () => {
@ -930,8 +938,8 @@ describe('type check blocks', () => {
TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true};
const block = tcb(TEMPLATE, DIRECTIVES, enableChecks);
expect(block).toContain(
'var _t2: Dir = (null!); ' +
'_t2.fieldA = (((ctx).foo)); ');
'var _t1: Dir = (null!); ' +
'_t1.fieldA = (((ctx).foo)); ');
});
});
});

View File

@ -66,7 +66,7 @@ runInEachFileSystem(os => {
const file1 = absoluteFrom('/file1.ts');
const file2 = absoluteFrom('/file2.ts');
const {program, templateTypeChecker, programStrategy} = setup([
{fileName: file1, templates: {'Cmp1': '<div></div>'}},
{fileName: file1, templates: {'Cmp1': '<div>{{value}}</div>'}},
{fileName: file2, templates: {'Cmp2': '<span></span>'}}
]);
@ -74,7 +74,7 @@ runInEachFileSystem(os => {
const block = templateTypeChecker.getTypeCheckBlock(cmp1);
expect(block).not.toBeNull();
expect(block!.getText()).toMatch(/: i[0-9]\.Cmp1/);
expect(block!.getText()).toContain(`document.createElement("div")`);
expect(block!.getText()).toContain(`value`);
});
it('should clear old inlines when necessary', () => {
@ -223,43 +223,43 @@ runInEachFileSystem(os => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {'Cmp': '<div></div>'},
templates: {'Cmp': '<div>{{original}}</div>'},
}]);
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!;
expect(tcbReal.getText()).toContain('div');
expect(tcbReal.getText()).toContain('original');
templateTypeChecker.overrideComponentTemplate(cmp, '<span></span>');
templateTypeChecker.overrideComponentTemplate(cmp, '<div>{{override}}</div>');
const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp);
expect(tcbOverridden).not.toBeNull();
expect(tcbOverridden!.getText()).not.toContain('div');
expect(tcbOverridden!.getText()).toContain('span');
expect(tcbOverridden!.getText()).not.toContain('original');
expect(tcbOverridden!.getText()).toContain('override');
});
it('should clear overrides on request', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {'Cmp': '<div></div>'},
templates: {'Cmp': '<div>{{original}}</div>'},
}]);
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
templateTypeChecker.overrideComponentTemplate(cmp, '<span></span>');
templateTypeChecker.overrideComponentTemplate(cmp, '<div>{{override}}</div>');
const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp)!;
expect(tcbOverridden.getText()).not.toContain('div');
expect(tcbOverridden.getText()).toContain('span');
expect(tcbOverridden.getText()).not.toContain('original');
expect(tcbOverridden.getText()).toContain('override');
templateTypeChecker.resetOverrides();
// The template should be back to the original, which has <div> and not <span>.
// The template should be back to the original.
const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!;
expect(tcbReal.getText()).toContain('div');
expect(tcbReal.getText()).not.toContain('span');
expect(tcbReal.getText()).toContain('original');
expect(tcbReal.getText()).not.toContain('override');
});
it('should override a template and make use of previously unused directives', () => {