Skip to content

Commit e94975d

Browse files
committed
fix(ivy): check semantics of NgModule for consistency (angular#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 angular#27604
1 parent d132bae commit e94975d

23 files changed

+640
-379
lines changed

packages/core/src/core_render3_private_export.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export {
128128
compileNgModule as ɵcompileNgModule,
129129
compileNgModuleDefs as ɵcompileNgModuleDefs,
130130
patchComponentDefWithScope as ɵpatchComponentDefWithScope,
131+
resetCompiledComponents as ɵresetCompiledComponents,
131132
} from './render3/jit/module';
132133
export {
133134
compilePipe as ɵcompilePipe,
@@ -224,6 +225,8 @@ export {
224225
SWITCH_RENDERER2_FACTORY__POST_R3__ as ɵSWITCH_RENDERER2_FACTORY__POST_R3__,
225226
} from './render/api';
226227

228+
export {getModuleFactory__POST_R3__ as ɵgetModuleFactory__POST_R3__} from './linker/ng_module_factory_loader';
229+
227230
export {
228231
publishGlobalUtil as ɵpublishGlobalUtil,
229232
publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils

packages/core/src/di/defs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export function defineInjector(options: {factory: () => any, providers?: any[],
168168
* @param type type which may have `ngInjectableDef`
169169
*/
170170
export function getInjectableDef<T>(type: any): InjectableDef<T>|null {
171-
return type.hasOwnProperty(NG_INJECTABLE_DEF) ? (type as any)[NG_INJECTABLE_DEF] : null;
171+
return type && type.hasOwnProperty(NG_INJECTABLE_DEF) ? (type as any)[NG_INJECTABLE_DEF] : null;
172172
}
173173

174174
/**
@@ -177,5 +177,5 @@ export function getInjectableDef<T>(type: any): InjectableDef<T>|null {
177177
* @param type type which may have `ngInjectorDef`
178178
*/
179179
export function getInjectorDef<T>(type: any): InjectorDef<T>|null {
180-
return type.hasOwnProperty(NG_INJECTOR_DEF) ? (type as any)[NG_INJECTOR_DEF] : null;
180+
return type && type.hasOwnProperty(NG_INJECTOR_DEF) ? (type as any)[NG_INJECTOR_DEF] : null;
181181
}

packages/core/src/di/r3_injector.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ export class R3Injector {
111111
const dedupStack: InjectorType<any>[] = [];
112112
deepForEach([def], injectorDef => this.processInjectorType(injectorDef, [], dedupStack));
113113

114-
additionalProviders &&
115-
deepForEach(additionalProviders, provider => this.processProvider(provider));
114+
additionalProviders && deepForEach(
115+
additionalProviders, provider => this.processProvider(
116+
provider, def, additionalProviders));
116117

117118

118119
// Make sure the INJECTOR token provides this injector.
@@ -270,25 +271,31 @@ export class R3Injector {
270271
}
271272

272273
// Next, include providers listed on the definition itself.
273-
if (def.providers != null && !isDuplicate) {
274-
deepForEach(def.providers, provider => this.processProvider(provider));
274+
const defProviders = def.providers;
275+
if (defProviders != null && !isDuplicate) {
276+
const injectorType = defOrWrappedDef as InjectorType<any>;
277+
deepForEach(
278+
defProviders, provider => this.processProvider(provider, injectorType, defProviders));
275279
}
276280

277281
// Finally, include providers from an InjectorDefTypeWithProviders if there was one.
278-
deepForEach(providers, provider => this.processProvider(provider));
282+
const ngModuleType = (defOrWrappedDef as InjectorTypeWithProviders<any>).ngModule;
283+
deepForEach(providers, provider => this.processProvider(provider, ngModuleType, providers));
279284
}
280285

281286
/**
282287
* Process a `SingleProvider` and add it.
283288
*/
284-
private processProvider(provider: SingleProvider): void {
289+
private processProvider(
290+
provider: SingleProvider, ngModuleType: InjectorType<any>, providers: any[]): void {
285291
// Determine the token from the provider. Either it's its own token, or has a {provide: ...}
286292
// property.
287293
provider = resolveForwardRef(provider);
288-
let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide);
294+
let token: any =
295+
isTypeProvider(provider) ? provider : resolveForwardRef(provider && provider.provide);
289296

290297
// Construct a `Record` for the provider.
291-
const record = providerToRecord(provider);
298+
const record = providerToRecord(provider, ngModuleType, providers);
292299

293300
if (!isTypeProvider(provider) && provider.multi === true) {
294301
// If the provider indicates that it's a multi-provider, process it specially.
@@ -317,7 +324,7 @@ export class R3Injector {
317324

318325
private hydrate<T>(token: Type<T>|InjectionToken<T>, record: Record<T>): T {
319326
if (record.value === CIRCULAR) {
320-
throw new Error(`Circular dep for ${stringify(token)}`);
327+
throw new Error(`Cannot instantiate cyclic dependency! ${stringify(token)}`);
321328
} else if (record.value === NOT_YET) {
322329
record.value = CIRCULAR;
323330
record.value = record.factory !();
@@ -345,20 +352,25 @@ function injectableDefOrInjectorDefFactory(token: Type<any>| InjectionToken<any>
345352
const injectorDef = getInjectorDef(token as InjectorType<any>);
346353
if (injectorDef !== null) {
347354
return injectorDef.factory;
348-
}
349-
350-
if (token instanceof InjectionToken) {
355+
} else if (token instanceof InjectionToken) {
351356
throw new Error(`Token ${stringify(token)} is missing an ngInjectableDef definition.`);
357+
} else if (token instanceof Function) {
358+
const paramLength = token.length;
359+
if (paramLength > 0) {
360+
const args: string[] = new Array(paramLength).fill('?');
361+
throw new Error(
362+
`Can't resolve all parameters for ${stringify(token)}: (${args.join(', ')}).`);
363+
}
364+
return () => new (token as Type<any>)();
352365
}
353-
// TODO(alxhub): there should probably be a strict mode which throws here instead of assuming a
354-
// no-args constructor.
355-
return () => new (token as Type<any>)();
366+
throw new Error('unreachable');
356367
}
357368
return injectableDef.factory;
358369
}
359370

360-
function providerToRecord(provider: SingleProvider): Record<any> {
361-
let factory: (() => any)|undefined = providerToFactory(provider);
371+
function providerToRecord(
372+
provider: SingleProvider, ngModuleType: InjectorType<any>, providers: any[]): Record<any> {
373+
let factory: (() => any)|undefined = providerToFactory(provider, ngModuleType, providers);
362374
if (isValueProvider(provider)) {
363375
return makeRecord(undefined, provider.useValue);
364376
} else {
@@ -371,7 +383,8 @@ function providerToRecord(provider: SingleProvider): Record<any> {
371383
*
372384
* @param provider provider to convert to factory
373385
*/
374-
export function providerToFactory(provider: SingleProvider): () => any {
386+
export function providerToFactory(
387+
provider: SingleProvider, ngModuleType?: InjectorType<any>, providers?: any[]): () => any {
375388
let factory: (() => any)|undefined = undefined;
376389
if (isTypeProvider(provider)) {
377390
return injectableDefOrInjectorDefFactory(resolveForwardRef(provider));
@@ -384,7 +397,18 @@ export function providerToFactory(provider: SingleProvider): () => any {
384397
factory = () => provider.useFactory(...injectArgs(provider.deps || []));
385398
} else {
386399
const classRef = resolveForwardRef(
387-
(provider as StaticClassProvider | ClassProvider).useClass || provider.provide);
400+
provider &&
401+
((provider as StaticClassProvider | ClassProvider).useClass || provider.provide));
402+
if (!classRef) {
403+
let ngModuleDetail = '';
404+
if (ngModuleType && providers) {
405+
const providerDetail = providers.map(v => v == provider ? '?' + provider + '?' : '...');
406+
ngModuleDetail =
407+
` - only instances of Provider and Type are allowed, got: [${providerDetail.join(', ')}]`;
408+
}
409+
throw new Error(
410+
`Invalid provider for the NgModule '${stringify(ngModuleType)}'` + ngModuleDetail);
411+
}
388412
if (hasDeps(provider)) {
389413
factory = () => new (classRef)(...injectArgs(provider.deps));
390414
} else {
@@ -409,15 +433,15 @@ function deepForEach<T>(input: (T | any[])[], fn: (value: T) => void): void {
409433
}
410434

411435
function isValueProvider(value: SingleProvider): value is ValueProvider {
412-
return USE_VALUE in value;
436+
return value && typeof value == 'object' && USE_VALUE in value;
413437
}
414438

415439
function isExistingProvider(value: SingleProvider): value is ExistingProvider {
416-
return !!(value as ExistingProvider).useExisting;
440+
return !!(value && (value as ExistingProvider).useExisting);
417441
}
418442

419443
function isFactoryProvider(value: SingleProvider): value is FactoryProvider {
420-
return !!(value as FactoryProvider).useFactory;
444+
return !!(value && (value as FactoryProvider).useFactory);
421445
}
422446

423447
export function isTypeProvider(value: SingleProvider): value is TypeProvider {

packages/core/src/linker/ng_module_factory_loader.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {NgModuleFactory as R3NgModuleFactory, NgModuleType} from '../render3/ng_module_ref';
10+
import {Type} from '../type';
11+
import {stringify} from '../util';
912
import {NgModuleFactory} from './ng_module_factory';
1013

1114
/**
@@ -17,23 +20,50 @@ export abstract class NgModuleFactoryLoader {
1720
abstract load(path: string): Promise<NgModuleFactory<any>>;
1821
}
1922

20-
let moduleFactories = new Map<string, NgModuleFactory<any>>();
23+
/**
24+
* Map of module-id to the corresponding NgModule.
25+
* - In pre Ivy we track NgModuleFactory,
26+
* - In post Ivy we track the NgModuleType
27+
*/
28+
const modules = new Map<string, NgModuleFactory<any>|NgModuleType>();
2129

2230
/**
2331
* Registers a loaded module. Should only be called from generated NgModuleFactory code.
2432
* @publicApi
2533
*/
2634
export function registerModuleFactory(id: string, factory: NgModuleFactory<any>) {
27-
const existing = moduleFactories.get(id);
28-
if (existing) {
29-
throw new Error(`Duplicate module registered for ${id
30-
} - ${existing.moduleType.name} vs ${factory.moduleType.name}`);
35+
const existing = modules.get(id) as NgModuleFactory<any>;
36+
assertNotExisting(id, existing && existing.moduleType);
37+
modules.set(id, factory);
38+
}
39+
40+
function assertNotExisting(id: string, type: Type<any>| null): void {
41+
if (type) {
42+
throw new Error(
43+
`Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`);
3144
}
32-
moduleFactories.set(id, factory);
3345
}
3446

35-
export function clearModulesForTest() {
36-
moduleFactories = new Map<string, NgModuleFactory<any>>();
47+
export function registerNgModuleType(id: string, ngModuleType: NgModuleType) {
48+
const existing = modules.get(id) as NgModuleType | null;
49+
assertNotExisting(id, existing);
50+
modules.set(id, ngModuleType);
51+
}
52+
53+
export function clearModulesForTest(): void {
54+
modules.clear();
55+
}
56+
57+
export function getModuleFactory__PRE_R3__(id: string): NgModuleFactory<any> {
58+
const factory = modules.get(id) as NgModuleFactory<any>| null;
59+
if (!factory) throw noModuleError(id);
60+
return factory;
61+
}
62+
63+
export function getModuleFactory__POST_R3__(id: string): NgModuleFactory<any> {
64+
const type = modules.get(id) as NgModuleType | null;
65+
if (!type) throw noModuleError(id);
66+
return new R3NgModuleFactory(type);
3767
}
3868

3969
/**
@@ -42,8 +72,8 @@ export function clearModulesForTest() {
4272
* cannot be found.
4373
* @publicApi
4474
*/
45-
export function getModuleFactory(id: string): NgModuleFactory<any> {
46-
const factory = moduleFactories.get(id);
47-
if (!factory) throw new Error(`No module with ID ${id} loaded`);
48-
return factory;
75+
export const getModuleFactory: (id: string) => NgModuleFactory<any> = getModuleFactory__PRE_R3__;
76+
77+
function noModuleError(id: string, ): Error {
78+
return new Error(`No module with ID ${id} loaded`);
4979
}

packages/core/src/metadata/ng_module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ApplicationRef} from '../application_ref';
1010
import {InjectorType, defineInjector} from '../di/defs';
1111
import {Provider} from '../di/provider';
1212
import {convertInjectableProviderToFactory} from '../di/util';
13+
import {NgModuleType} from '../render3';
1314
import {compileNgModule as render3CompileNgModule} from '../render3/jit/module';
1415
import {Type} from '../type';
1516
import {TypeDecorator, makeDecorator} from '../util/decorators';
@@ -337,7 +338,7 @@ export const NgModule: NgModuleDecorator = makeDecorator(
337338
* * The `imports` and `exports` options bring in members from other modules, and make
338339
* this module's members available to others.
339340
*/
340-
(type: Type<any>, meta: NgModule) => SWITCH_COMPILE_NGMODULE(type, meta));
341+
(type: NgModuleType, meta: NgModule) => SWITCH_COMPILE_NGMODULE(type, meta));
341342

342343
/**
343344
* @description

packages/core/src/render3/definition.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {Provider} from '../di/provider';
1313
import {NgModuleDef} from '../metadata/ng_module';
1414
import {ViewEncapsulation} from '../metadata/view';
1515
import {Mutable, Type} from '../type';
16-
import {noSideEffects} from '../util';
16+
import {noSideEffects, stringify} from '../util';
1717

1818
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
1919
import {BaseDef, ComponentDef, ComponentDefFeature, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition';
@@ -651,6 +651,12 @@ export function getPipeDef<T>(type: any): PipeDef<T>|null {
651651
return (type as any)[NG_PIPE_DEF] || null;
652652
}
653653

654-
export function getNgModuleDef<T>(type: any): NgModuleDef<T>|null {
655-
return (type as any)[NG_MODULE_DEF] || null;
654+
export function getNgModuleDef<T>(type: any, throwNotFound: true): NgModuleDef<T>;
655+
export function getNgModuleDef<T>(type: any): NgModuleDef<T>|null;
656+
export function getNgModuleDef<T>(type: any, throwNotFound?: boolean): NgModuleDef<T>|null {
657+
const ngModuleDef = (type as any)[NG_MODULE_DEF] || null;
658+
if (!ngModuleDef && throwNotFound === true) {
659+
throw new Error(`Type ${stringify(type)} does not have 'ngModuleDef' property.`);
660+
}
661+
return ngModuleDef;
656662
}

packages/core/src/render3/jit/compiler_facade_interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ export interface R3InjectorMetadataFacade {
101101
name: string;
102102
type: any;
103103
deps: R3DependencyMetadataFacade[]|null;
104-
providers: any;
105-
imports: any;
104+
providers: any[];
105+
imports: any[];
106106
}
107107

108108
export interface R3DirectiveMetadataFacade {

packages/core/src/render3/jit/directive.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ComponentType} from '..';
910
import {Query} from '../../metadata/di';
1011
import {Component, Directive} from '../../metadata/directives';
1112
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
@@ -58,7 +59,7 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
5859
preserveWhitespaces: metadata.preserveWhitespaces || false,
5960
styles: metadata.styles || EMPTY_ARRAY,
6061
animations: metadata.animations,
61-
viewQueries: extractQueriesMetadata(getReflect().propMetadata(type), isViewQuery),
62+
viewQueries: extractQueriesMetadata(type, getReflect().propMetadata(type), isViewQuery),
6263
directives: [],
6364
pipes: new Map(),
6465
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
@@ -108,7 +109,7 @@ export function compileDirective(type: Type<any>, directive: Directive): void {
108109
Object.defineProperty(type, NG_DIRECTIVE_DEF, {
109110
get: () => {
110111
if (ngDirectiveDef === null) {
111-
const facade = directiveMetadata(type, directive);
112+
const facade = directiveMetadata(type as ComponentType<any>, directive);
112113
ngDirectiveDef = getCompilerFacade().compileDirective(
113114
angularCoreEnv, `ng://${type && type.name}/ngDirectiveDef.js`, facade);
114115
}
@@ -141,7 +142,7 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
141142
propMetadata: propMetadata,
142143
inputs: metadata.inputs || EMPTY_ARRAY,
143144
outputs: metadata.outputs || EMPTY_ARRAY,
144-
queries: extractQueriesMetadata(propMetadata, isContentQuery),
145+
queries: extractQueriesMetadata(type, propMetadata, isContentQuery),
145146
lifecycle: {
146147
usesOnChanges: type.prototype.ngOnChanges !== undefined,
147148
},
@@ -168,13 +169,18 @@ export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3Qu
168169
};
169170
}
170171
function extractQueriesMetadata(
171-
propMetadata: {[key: string]: any[]},
172+
type: Type<any>, propMetadata: {[key: string]: any[]},
172173
isQueryAnn: (ann: any) => ann is Query): R3QueryMetadataFacade[] {
173174
const queriesMeta: R3QueryMetadataFacade[] = [];
174175
for (const field in propMetadata) {
175176
if (propMetadata.hasOwnProperty(field)) {
176177
propMetadata[field].forEach(ann => {
177178
if (isQueryAnn(ann)) {
179+
if (!ann.selector) {
180+
throw new Error(
181+
`Can't construct a query for the property "${field}" of ` +
182+
`"${stringify(type)}" since the query selector wasn't defined.`);
183+
}
178184
queriesMeta.push(convertToR3QueryMetadata(field, ann));
179185
}
180186
});

0 commit comments

Comments
 (0)