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:
parent
175c79d1d8
commit
f42e6ce917
|
@ -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.
|
||||
|
|
|
@ -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*/);');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)); ');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue