fix(ivy): check semantics of NgModule for consistency (#27604)

`NgModule` requires that `Component`s/`Directive`s/`Pipe`s are listed in
declarations, and that each `Component`s/`Directive`s/`Pipe` is declared
in exactly one `NgModule`. This change adds runtime checks to ensure
that these sementics are true at runtime.

There will need to be seperate set of checks for the AoT path of the
codebase to verify that same set of semantics hold. Due to current
design there does not seem to be an easy way to share the two checks
because JIT deal with references where as AoT deals with AST nodes.

PR Close #27604
This commit is contained in:
Miško Hevery
2018-12-11 10:43:02 -08:00
parent d132baede3
commit e94975d109
23 changed files with 627 additions and 366 deletions

View File

@ -101,8 +101,8 @@ export interface R3InjectorMetadataFacade {
name: string;
type: any;
deps: R3DependencyMetadataFacade[]|null;
providers: any;
imports: any;
providers: any[];
imports: any[];
}
export interface R3DirectiveMetadataFacade {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentType} from '..';
import {Query} from '../../metadata/di';
import {Component, Directive} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
@ -58,7 +59,7 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
preserveWhitespaces: metadata.preserveWhitespaces || false,
styles: metadata.styles || EMPTY_ARRAY,
animations: metadata.animations,
viewQueries: extractQueriesMetadata(getReflect().propMetadata(type), isViewQuery),
viewQueries: extractQueriesMetadata(type, getReflect().propMetadata(type), isViewQuery),
directives: [],
pipes: new Map(),
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
@ -108,7 +109,7 @@ export function compileDirective(type: Type<any>, directive: Directive): void {
Object.defineProperty(type, NG_DIRECTIVE_DEF, {
get: () => {
if (ngDirectiveDef === null) {
const facade = directiveMetadata(type, directive);
const facade = directiveMetadata(type as ComponentType<any>, directive);
ngDirectiveDef = getCompilerFacade().compileDirective(
angularCoreEnv, `ng://${type && type.name}/ngDirectiveDef.js`, facade);
}
@ -141,7 +142,7 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
propMetadata: propMetadata,
inputs: metadata.inputs || EMPTY_ARRAY,
outputs: metadata.outputs || EMPTY_ARRAY,
queries: extractQueriesMetadata(propMetadata, isContentQuery),
queries: extractQueriesMetadata(type, propMetadata, isContentQuery),
lifecycle: {
usesOnChanges: type.prototype.ngOnChanges !== undefined,
},
@ -168,13 +169,18 @@ export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3Qu
};
}
function extractQueriesMetadata(
propMetadata: {[key: string]: any[]},
type: Type<any>, propMetadata: {[key: string]: any[]},
isQueryAnn: (ann: any) => ann is Query): R3QueryMetadataFacade[] {
const queriesMeta: R3QueryMetadataFacade[] = [];
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isQueryAnn(ann)) {
if (!ann.selector) {
throw new Error(
`Can't construct a query for the property "${field}" of ` +
`"${stringify(type)}" since the query selector wasn't defined.`);
}
queriesMeta.push(convertToR3QueryMetadata(field, ann));
}
});

View File

@ -7,12 +7,16 @@
*/
import {resolveForwardRef} from '../../di/forward_ref';
import {registerNgModuleType} from '../../linker/ng_module_factory_loader';
import {Component} from '../../metadata';
import {ModuleWithProviders, NgModule, NgModuleDef, NgModuleTransitiveScopes} from '../../metadata/ng_module';
import {Type} from '../../type';
import {assertDefined} from '../assert';
import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef} from '../definition';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_INJECTOR_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from '../fields';
import {ComponentDef} from '../interfaces/definition';
import {NgModuleType} from '../ng_module_ref';
import {stringify} from '../util';
import {R3InjectorMetadataFacade, getCompilerFacade} from './compiler_facade';
import {angularCoreEnv} from './environment';
@ -44,16 +48,19 @@ let flushingModuleQueue = false;
export function flushModuleScopingQueueAsMuchAsPossible() {
if (!flushingModuleQueue) {
flushingModuleQueue = true;
for (let i = moduleQueue.length - 1; i >= 0; i--) {
const {moduleType, ngModule} = moduleQueue[i];
try {
for (let i = moduleQueue.length - 1; i >= 0; i--) {
const {moduleType, ngModule} = moduleQueue[i];
if (ngModule.declarations && ngModule.declarations.every(isResolvedDeclaration)) {
// dequeue
moduleQueue.splice(i, 1);
setScopeOnDeclaredComponents(moduleType, ngModule);
if (ngModule.declarations && ngModule.declarations.every(isResolvedDeclaration)) {
// dequeue
moduleQueue.splice(i, 1);
setScopeOnDeclaredComponents(moduleType, ngModule);
}
}
} finally {
flushingModuleQueue = false;
}
flushingModuleQueue = false;
}
}
@ -75,7 +82,7 @@ function isResolvedDeclaration(declaration: any[] | Type<any>): boolean {
* This function automatically gets called when a class has a `@NgModule` decorator.
*/
export function compileNgModule(moduleType: Type<any>, ngModule: NgModule = {}): void {
compileNgModuleDefs(moduleType, ngModule);
compileNgModuleDefs(moduleType as NgModuleType, ngModule);
// Because we don't know if all declarations have resolved yet at the moment the
// NgModule decorator is executing, we're enqueueing the setting of module scope
@ -87,7 +94,7 @@ export function compileNgModule(moduleType: Type<any>, ngModule: NgModule = {}):
/**
* Compiles and adds the `ngModuleDef` and `ngInjectorDef` properties to the module class.
*/
export function compileNgModuleDefs(moduleType: Type<any>, ngModule: NgModule): void {
export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule): void {
ngDevMode && assertDefined(moduleType, 'Required value moduleType');
ngDevMode && assertDefined(ngModule, 'Required value ngModule');
const declarations: Type<any>[] = flatten(ngModule.declarations || EMPTY_ARRAY);
@ -110,11 +117,15 @@ export function compileNgModuleDefs(moduleType: Type<any>, ngModule: NgModule):
return ngModuleDef;
}
});
if (ngModule.id) {
registerNgModuleType(ngModule.id, moduleType);
}
let ngInjectorDef: any = null;
Object.defineProperty(moduleType, NG_INJECTOR_DEF, {
get: () => {
if (ngInjectorDef === null) {
ngDevMode && verifySemanticsOfNgModuleDef(moduleType as any as NgModuleType);
const meta: R3InjectorMetadataFacade = {
name: moduleType.name,
type: moduleType,
@ -135,6 +146,164 @@ export function compileNgModuleDefs(moduleType: Type<any>, ngModule: NgModule):
});
}
function verifySemanticsOfNgModuleDef(moduleType: NgModuleType): void {
if (verifiedNgModule.get(moduleType)) return;
verifiedNgModule.set(moduleType, true);
moduleType = resolveForwardRef(moduleType);
const ngModuleDef = getNgModuleDef(moduleType, true);
const errors: string[] = [];
ngModuleDef.declarations.forEach(verifyDeclarationsHaveDefinitions);
const combinedDeclarations: Type<any>[] = [
...ngModuleDef.declarations, //
...flatten(ngModuleDef.imports.map(computeCombinedExports)),
];
ngModuleDef.exports.forEach(verifyExportsAreDeclaredOrReExported);
ngModuleDef.declarations.forEach(verifyDeclarationIsUnique);
ngModuleDef.declarations.forEach(verifyComponentEntryComponentsIsPartOfNgModule);
const ngModule = getAnnotation<NgModule>(moduleType, 'NgModule');
if (ngModule) {
ngModule.imports &&
flatten(ngModule.imports, unwrapModuleWithProvidersImports)
.forEach(verifySemanticsOfNgModuleDef);
ngModule.bootstrap && ngModule.bootstrap.forEach(verifyComponentIsPartOfNgModule);
ngModule.entryComponents && ngModule.entryComponents.forEach(verifyComponentIsPartOfNgModule);
}
// Throw Error if any errors were detected.
if (errors.length) {
throw new Error(errors.join('\n'));
}
////////////////////////////////////////////////////////////////////////////////////////////////
function verifyDeclarationsHaveDefinitions(type: Type<any>): void {
type = resolveForwardRef(type);
const def = getComponentDef(type) || getDirectiveDef(type) || getPipeDef(type);
if (!def) {
errors.push(
`Unexpected value '${stringify(type)}' declared by the module '${stringify(moduleType)}'. Please add a @Pipe/@Directive/@Component annotation.`);
}
}
function verifyExportsAreDeclaredOrReExported(type: Type<any>) {
type = resolveForwardRef(type);
const kind = getComponentDef(type) && 'component' || getDirectiveDef(type) && 'directive' ||
getPipeDef(type) && 'pipe';
if (kind) {
// only checked if we are declared as Component, Directive, or Pipe
// Modules don't need to be declared or imported.
if (combinedDeclarations.lastIndexOf(type) === -1) {
// We are exporting something which we don't explicitly declare or import.
errors.push(
`Can't export ${kind} ${stringify(type)} from ${stringify(moduleType)} as it was neither declared nor imported!`);
}
}
}
function verifyDeclarationIsUnique(type: Type<any>) {
type = resolveForwardRef(type);
const existingModule = ownerNgModule.get(type);
if (existingModule && existingModule !== moduleType) {
const modules = [existingModule, moduleType].map(stringify).sort();
errors.push(
`Type ${stringify(type)} is part of the declarations of 2 modules: ${modules[0]} and ${modules[1]}! ` +
`Please consider moving ${stringify(type)} to a higher module that imports ${modules[0]} and ${modules[1]}. ` +
`You can also create a new NgModule that exports and includes ${stringify(type)} then import that NgModule in ${modules[0]} and ${modules[1]}.`);
} else {
// Mark type as having owner.
ownerNgModule.set(type, moduleType);
}
}
function verifyComponentIsPartOfNgModule(type: Type<any>) {
type = resolveForwardRef(type);
const existingModule = ownerNgModule.get(type);
if (!existingModule) {
errors.push(
`Component ${stringify(type)} is not part of any NgModule or the module has not been imported into your module.`);
}
}
function verifyComponentEntryComponentsIsPartOfNgModule(type: Type<any>) {
type = resolveForwardRef(type);
if (getComponentDef(type)) {
// We know we are component
const component = getAnnotation<Component>(type, 'Component');
if (component && component.entryComponents) {
component.entryComponents.forEach(verifyComponentIsPartOfNgModule);
}
}
}
}
function unwrapModuleWithProvidersImports(
typeOrWithProviders: NgModuleType<any>| {ngModule: NgModuleType<any>}): NgModuleType<any> {
typeOrWithProviders = resolveForwardRef(typeOrWithProviders);
return (typeOrWithProviders as any).ngModule || typeOrWithProviders;
}
function getAnnotation<T>(type: any, name: string): T|null {
let annotation: T|null = null;
collect(type.__annotations__);
collect(type.decorators);
return annotation;
function collect(annotations: any[] | null) {
if (annotations) {
annotations.forEach(readAnnotation);
}
}
function readAnnotation(
decorator: {type: {prototype: {ngMetadataName: string}, args: any[]}, args: any}): void {
if (!annotation) {
const proto = Object.getPrototypeOf(decorator);
if (proto.ngMetadataName == name) {
annotation = decorator as any;
} else if (decorator.type) {
const proto = Object.getPrototypeOf(decorator.type);
if (proto.ngMetadataName == name) {
annotation = decorator.args[0];
}
}
}
}
}
/**
* Keep track of compiled components. This is needed because in tests we often want to compile the
* same component with more than one NgModule. This would cause an error unless we reset which
* NgModule the component belongs to. We keep the list of compiled components here so that the
* TestBed can reset it later.
*/
let ownerNgModule = new Map<Type<any>, NgModuleType<any>>();
let verifiedNgModule = new Map<NgModuleType<any>, boolean>();
export function resetCompiledComponents(): void {
ownerNgModule = new Map<Type<any>, NgModuleType<any>>();
verifiedNgModule = new Map<NgModuleType<any>, boolean>();
moduleQueue.length = 0;
}
/**
* Computes the combined declarations of explicit declarations, as well as declarations inherited
* by
* traversing the exports of imported modules.
* @param type
*/
function computeCombinedExports(type: Type<any>): Type<any>[] {
type = resolveForwardRef(type);
const ngModuleDef = getNgModuleDef(type, true);
return [...flatten(ngModuleDef.exports.map((type) => {
const ngModuleDef = getNgModuleDef(type);
if (ngModuleDef) {
verifySemanticsOfNgModuleDef(type as any as NgModuleType);
return computeCombinedExports(type);
} else {
return type;
}
}))];
}
/**
* Some declared components may be compiled asynchronously, and thus may not have their
* ngComponentDef set yet. If this is the case, then a reference to the module is written into
@ -264,13 +433,13 @@ export function transitiveScopesFor<T>(moduleType: Type<T>): NgModuleTransitiveS
return scopes;
}
function flatten<T>(values: any[]): T[] {
const out: T[] = [];
function flatten<T>(values: any[], mapFn?: (value: T) => any): Type<T>[] {
const out: Type<T>[] = [];
values.forEach(value => {
if (Array.isArray(value)) {
out.push(...flatten<T>(value));
out.push(...flatten<T>(value, mapFn));
} else {
out.push(value);
out.push(mapFn ? mapFn(value) : value);
}
});
return out;