fix(ivy): validate the NgModule declarations field (#34404)
This commit adds three previously missing validations to NgModule.declarations: 1. It checks that declared classes are actually within the current compilation. 2. It checks that declared classes are directives, components, or pipes. 3. It checks that classes are declared in at most one NgModule. PR Close #34404
This commit is contained in:

committed by
Kara Erickson

parent
9cabd6638e
commit
763f8d470a
@ -9,4 +9,4 @@
|
||||
export {ExportScope, ScopeData} from './src/api';
|
||||
export {ComponentScopeReader, CompoundComponentScopeReader} from './src/component_scope';
|
||||
export {DtsModuleScopeResolver, MetadataDtsModuleScopeResolver} from './src/dependency';
|
||||
export {LocalModuleScope, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local';
|
||||
export {DeclarationData, LocalModuleScope, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local';
|
||||
|
@ -75,7 +75,14 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
||||
* contain directives. This doesn't cause any problems but isn't useful as there is no concept of
|
||||
* a directive's compilation scope.
|
||||
*/
|
||||
private declarationToModule = new Map<ClassDeclaration, ClassDeclaration>();
|
||||
private declarationToModule = new Map<ClassDeclaration, DeclarationData>();
|
||||
|
||||
/**
|
||||
* This maps from the directive/pipe class to a map of data for each NgModule that declares the
|
||||
* directive/pipe. This data is needed to produce an error for the given class.
|
||||
*/
|
||||
private duplicateDeclarations =
|
||||
new Map<ClassDeclaration, Map<ClassDeclaration, DeclarationData>>();
|
||||
|
||||
private moduleToRef = new Map<ClassDeclaration, Reference<ClassDeclaration>>();
|
||||
|
||||
@ -111,9 +118,12 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
||||
*/
|
||||
registerNgModuleMetadata(data: NgModuleMeta): void {
|
||||
this.assertCollecting();
|
||||
const ngModule = data.ref.node;
|
||||
this.moduleToRef.set(data.ref.node, data.ref);
|
||||
// Iterate over the module's declarations, and add them to declarationToModule. If duplicates
|
||||
// are found, they're instead tracked in duplicateDeclarations.
|
||||
for (const decl of data.declarations) {
|
||||
this.declarationToModule.set(decl.node, data.ref.node);
|
||||
this.registerDeclarationOfModule(ngModule, decl, data.rawDeclarations);
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,10 +134,25 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
||||
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null {
|
||||
const scope = !this.declarationToModule.has(clazz) ?
|
||||
null :
|
||||
this.getScopeOfModule(this.declarationToModule.get(clazz) !);
|
||||
this.getScopeOfModule(this.declarationToModule.get(clazz) !.ngModule);
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `node` is declared in more than one NgModule (duplicate declaration), then get the
|
||||
* `DeclarationData` for each offending declaration.
|
||||
*
|
||||
* Ordinarily a class is only declared in one NgModule, in which case this function returns
|
||||
* `null`.
|
||||
*/
|
||||
getDuplicateDeclarations(node: ClassDeclaration): DeclarationData[]|null {
|
||||
if (!this.duplicateDeclarations.has(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Array.from(this.duplicateDeclarations.get(node) !.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects registered data for a module and its directives/pipes and convert it into a full
|
||||
* `LocalModuleScope`.
|
||||
@ -165,15 +190,51 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
||||
*/
|
||||
getCompilationScopes(): CompilationScope[] {
|
||||
const scopes: CompilationScope[] = [];
|
||||
this.declarationToModule.forEach((ngModule, declaration) => {
|
||||
const scope = this.getScopeOfModule(ngModule);
|
||||
this.declarationToModule.forEach((declData, declaration) => {
|
||||
const scope = this.getScopeOfModule(declData.ngModule);
|
||||
if (scope !== null) {
|
||||
scopes.push({declaration, ngModule, ...scope.compilation});
|
||||
scopes.push({declaration, ngModule: declData.ngModule, ...scope.compilation});
|
||||
}
|
||||
});
|
||||
return scopes;
|
||||
}
|
||||
|
||||
private registerDeclarationOfModule(
|
||||
ngModule: ClassDeclaration, decl: Reference<ClassDeclaration>,
|
||||
rawDeclarations: ts.Expression|null): void {
|
||||
const declData: DeclarationData = {
|
||||
ngModule,
|
||||
ref: decl, rawDeclarations,
|
||||
};
|
||||
|
||||
// First, check for duplicate declarations of the same directive/pipe.
|
||||
if (this.duplicateDeclarations.has(decl.node)) {
|
||||
// This directive/pipe has already been identified as being duplicated. Add this module to the
|
||||
// map of modules for which a duplicate declaration exists.
|
||||
this.duplicateDeclarations.get(decl.node) !.set(ngModule, declData);
|
||||
} else if (
|
||||
this.declarationToModule.has(decl.node) &&
|
||||
this.declarationToModule.get(decl.node) !.ngModule !== ngModule) {
|
||||
// This directive/pipe is already registered as declared in another module. Mark it as a
|
||||
// duplicate instead.
|
||||
const duplicateDeclMap = new Map<ClassDeclaration, DeclarationData>();
|
||||
const firstDeclData = this.declarationToModule.get(decl.node) !;
|
||||
|
||||
// Being detected as a duplicate means there are two NgModules (for now) which declare this
|
||||
// directive/pipe. Add both of them to the duplicate tracking map.
|
||||
duplicateDeclMap.set(firstDeclData.ngModule, firstDeclData);
|
||||
duplicateDeclMap.set(ngModule, declData);
|
||||
this.duplicateDeclarations.set(decl.node, duplicateDeclMap);
|
||||
|
||||
// Remove the directive/pipe from `declarationToModule` as it's a duplicate declaration, and
|
||||
// therefore not valid.
|
||||
this.declarationToModule.delete(decl.node);
|
||||
} else {
|
||||
// This is the first declaration of this directive/pipe, so map it.
|
||||
this.declarationToModule.set(decl.node, declData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of `getScopeOfModule` which accepts a reference to a class and differentiates
|
||||
* between:
|
||||
@ -514,3 +575,9 @@ function reexportCollision(
|
||||
{node: refB.node.name, messageText: childMessageText},
|
||||
]);
|
||||
}
|
||||
|
||||
export interface DeclarationData {
|
||||
ngModule: ClassDeclaration;
|
||||
ref: Reference;
|
||||
rawDeclarations: ts.Expression|null;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
declarations: [Dir1, Dir2, Pipe1],
|
||||
exports: [Dir1, Pipe1],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scope = scopeRegistry.getScopeOfModule(Module.node) !;
|
||||
@ -69,6 +70,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
declarations: [DirA],
|
||||
exports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleB.node),
|
||||
@ -76,6 +78,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
declarations: [DirB],
|
||||
imports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleC.node),
|
||||
@ -83,6 +86,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
exports: [DirCE],
|
||||
imports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !;
|
||||
@ -99,6 +103,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [],
|
||||
declarations: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleB.node),
|
||||
@ -106,6 +111,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
exports: [Dir],
|
||||
imports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !;
|
||||
@ -122,6 +128,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [ModuleB, ModuleC],
|
||||
exports: [DirA, DirA, DirB, ModuleB],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleB.node),
|
||||
@ -129,6 +136,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [],
|
||||
exports: [DirB],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleC.node),
|
||||
@ -136,6 +144,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [],
|
||||
exports: [ModuleB],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scope = scopeRegistry.getScopeOfModule(ModuleA.node) !;
|
||||
@ -159,6 +168,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [],
|
||||
declarations: [DirInModule],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scope = scopeRegistry.getScopeOfModule(Module.node) !;
|
||||
@ -174,6 +184,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [ModuleB],
|
||||
declarations: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleB.node),
|
||||
@ -181,6 +192,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
exports: [Dir],
|
||||
imports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !;
|
||||
@ -196,6 +208,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
imports: [],
|
||||
declarations: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
metaRegistry.registerNgModuleMetadata({
|
||||
ref: new Reference(ModuleB.node),
|
||||
@ -203,6 +216,7 @@ describe('LocalModuleScopeRegistry', () => {
|
||||
exports: [Dir],
|
||||
imports: [],
|
||||
schemas: [],
|
||||
rawDeclarations: null,
|
||||
});
|
||||
|
||||
expect(scopeRegistry.getScopeOfModule(ModuleA.node)).toBe(null);
|
||||
|
Reference in New Issue
Block a user