fix(core): prevent unknown property check for AOT-compiled components (#36072)

Prior to this commit, the unknown property check was unnecessarily invoked for AOT-compiled components (for these components, the check happens at compile time). This commit updates the code to avoid unknown property verification for AOT-compiled components by checking whether schemas information is present (as a way to detect whether this is JIT or AOT compiled component).

Resolves #35945.

PR Close #36072
This commit is contained in:
Andrew Kushnir 2020-03-15 09:08:57 -07:00 committed by atscott
parent 88b0985bad
commit 4a9f0bebc3
2 changed files with 120 additions and 65 deletions

View File

@ -1038,6 +1038,12 @@ export function setNgReflectProperties(
function validateProperty( function validateProperty(
tView: TView, lView: LView, element: RElement|RComment, propName: string, tView: TView, lView: LView, element: RElement|RComment, propName: string,
tNode: TNode): boolean { tNode: TNode): boolean {
// If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT
// mode where this check happens at compile time. In JIT mode, `schemas` is always present and
// defined as an array (as an empty array in case `schemas` field is not defined) and we should
// execute the check below.
if (tView.schemas === null) return true;
// The property is considered valid if the element matches the schema, it exists on the element // The property is considered valid if the element matches the schema, it exists on the element
// or it is synthetic, and we are in a browser context (web worker nodes should be skipped). // or it is synthetic, and we are in a browser context (web worker nodes should be skipped).
if (matchingSchemas(tView, lView, tNode.tagName) || propName in element || if (matchingSchemas(tView, lView, tNode.tagName) || propName in element ||

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, CUSTOM_ELEMENTS_SCHEMA, Injectable, InjectionToken, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element} from '@angular/core'; import {Component, CUSTOM_ELEMENTS_SCHEMA, Injectable, InjectionToken, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element, ɵɵproperty as property} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {modifiedInIvy, onlyInIvy} from '@angular/private/testing'; import {modifiedInIvy, onlyInIvy} from '@angular/private/testing';
@ -247,73 +247,122 @@ describe('NgModule', () => {
}).toThrowError(/'custom' is not a known element/); }).toThrowError(/'custom' is not a known element/);
}); });
onlyInIvy('test relies on Ivy-specific AOT format') onlyInIvy('test relies on Ivy-specific AOT format').describe('AOT-compiled components', () => {
.it('should not log unknown element warning for AOT-compiled components', () => { function createComponent(
const spy = spyOn(console, 'warn'); template: (rf: any) => void, vars: number, consts?: (number|string)[][]) {
class Comp {
/* static ɵfac = () => new Comp();
* @Component({ static ɵcmp = defineComponent({
* selector: 'comp', type: Comp,
* template: '<custom-el></custom-el>', selectors: [['comp']],
* }) decls: 1,
* class MyComp {} vars,
*/ consts,
class MyComp { template,
static ɵfac = () => new MyComp(); encapsulation: 2
static ɵcmp = defineComponent({
type: MyComp,
selectors: [['comp']],
decls: 1,
vars: 0,
template:
function MyComp_Template(rf, ctx) {
if (rf & 1) {
element(0, 'custom-el');
}
},
encapsulation: 2
});
}
setClassMetadata(
MyComp, [{
type: Component,
args: [{
selector: 'comp',
template: '<custom-el></custom-el>',
}]
}],
null, null);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
class MyModule {
static ɵmod = defineNgModule({type: MyModule});
static ɵinj = defineInjector({factory: () => new MyModule()});
}
setClassMetadata(
MyModule, [{
type: NgModule,
args: [{
declarations: [MyComp],
schemas: [NO_ERRORS_SCHEMA],
}]
}],
null, null);
TestBed.configureTestingModule({
imports: [MyModule],
}); });
}
setClassMetadata(
Comp, [{
type: Component,
args: [
{selector: 'comp', template: '...'},
]
}],
null, null);
return Comp;
}
const fixture = TestBed.createComponent(MyComp); function createNgModule(Comp: any) {
fixture.detectChanges(); class Module {
expect(spy).not.toHaveBeenCalled(); static ɵmod = defineNgModule({type: Module});
static ɵinj = defineInjector({factory: () => new Module()});
}
setClassMetadata(
Module, [{
type: NgModule,
args: [{
declarations: [Comp],
schemas: [NO_ERRORS_SCHEMA],
}]
}],
null, null);
return Module;
}
it('should not log unknown element warning for AOT-compiled components', () => {
const spy = spyOn(console, 'warn');
/*
* @Component({
* selector: 'comp',
* template: '<custom-el></custom-el>',
* })
* class MyComp {}
*/
const MyComp = createComponent((rf: any) => {
if (rf & 1) {
element(0, 'custom-el');
}
}, 0);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
const MyModule = createNgModule(MyComp);
TestBed.configureTestingModule({
imports: [MyModule],
}); });
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
});
it('should not log unknown property warning for AOT-compiled components', () => {
const spy = spyOn(console, 'warn');
/*
* @Component({
* selector: 'comp',
* template: '<div [foo]="1"></div>',
* })
* class MyComp {}
*/
const MyComp = createComponent((rf: any) => {
if (rf & 1) {
element(0, 'div', 0);
}
if (rf & 2) {
property('foo', true);
}
}, 1, [[3, 'foo']]);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
const MyModule = createNgModule(MyComp);
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
});
});
onlyInIvy('unknown element check logs a warning rather than throwing') onlyInIvy('unknown element check logs a warning rather than throwing')
.it('should not warn about unknown elements with CUSTOM_ELEMENTS_SCHEMA', () => { .it('should not warn about unknown elements with CUSTOM_ELEMENTS_SCHEMA', () => {
@Component({template: `<custom-el></custom-el>`}) @Component({template: `<custom-el></custom-el>`})
@ -519,4 +568,4 @@ describe('NgModule', () => {
TestBed.createComponent(TestCmp); TestBed.createComponent(TestCmp);
expect(componentInstance).toBeAnInstanceOf(TestCmp); expect(componentInstance).toBeAnInstanceOf(TestCmp);
}); });
}); });