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:
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user