fix(compiler): process imports first and declarations second while calculating scopes (#35850)

Prior to this commit, while calculating the scope for a module, Ivy compiler processed `declarations` field first and `imports` after that. That results in a couple issues:

* for Pipes with the same `name` and present in `declarations` and in an imported module, Pipe from imported module was selected. In View Engine the logic is opposite: Pipes from `declarations` field receive higher priority.
* for Directives with the same selector and present in `declarations` and in an imported module, we first invoked the logic of a Directive from `declarations` field and after that - imported Directive logic. In View Engine, it was the opposite and the logic of a Directive from the `declarations` field was invoked last.

In order to align Ivy and View Engine behavior, this commit updates the logic in which we populate module scope: we first process all imports and after that handle `declarations` field. As a result, in Ivy both use-cases listed above work similar to View Engine.

Resolves #35502.

PR Close #35850
This commit is contained in:
Andrew Kushnir
2020-03-03 18:06:16 -08:00
committed by Matias Niemelä
parent 191e4d15b5
commit 0bf6e58db2
5 changed files with 422 additions and 42 deletions

View File

@ -296,9 +296,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
const exportPipes = new Map<ts.Declaration, PipeMeta>();
// The algorithm is as follows:
// 1) Add directives/pipes declared in the NgModule to the compilation scope.
// 2) Add all of the directives/pipes from each NgModule imported into the current one to the
// compilation scope. At this point, the compilation scope is complete.
// 1) Add all of the directives/pipes from each NgModule imported into the current one to the
// compilation scope.
// 2) Add directives/pipes declared in the NgModule to the compilation scope. At this point, the
// compilation scope is complete.
// 3) For each entry in the NgModule's exports:
// a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is
// one, add them to the export scope of this NgModule.
@ -307,31 +308,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
// c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this
// is an error.
// 1) add declarations.
for (const decl of ngModule.declarations) {
const directive = this.localReader.getDirectiveMetadata(decl);
const pipe = this.localReader.getPipeMetadata(decl);
if (directive !== null) {
compilationDirectives.set(decl.node, {...directive, ref: decl});
} else if (pipe !== null) {
compilationPipes.set(decl.node, {...pipe, ref: decl});
} else {
this.taintedModules.add(ngModule.ref.node);
const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations !);
diagnostics.push(makeDiagnostic(
ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode,
`The class '${decl.node.name.text}' is listed in the declarations of the NgModule '${ngModule.ref.node.name.text}', but is not a directive, a component, or a pipe.
Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`,
[{node: decl.node.name, messageText: `'${decl.node.name.text}' is declared here.`}]));
continue;
}
declared.add(decl.node);
}
// 2) process imports.
// 1) process imports.
for (const decl of ngModule.imports) {
const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'import');
if (importScope === null) {
@ -353,6 +330,30 @@ Either remove it from the NgModule's declarations, or add an appropriate Angular
}
}
// 2) add declarations.
for (const decl of ngModule.declarations) {
const directive = this.localReader.getDirectiveMetadata(decl);
const pipe = this.localReader.getPipeMetadata(decl);
if (directive !== null) {
compilationDirectives.set(decl.node, {...directive, ref: decl});
} else if (pipe !== null) {
compilationPipes.set(decl.node, {...pipe, ref: decl});
} else {
this.taintedModules.add(ngModule.ref.node);
const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations !);
diagnostics.push(makeDiagnostic(
ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode,
`The class '${decl.node.name.text}' is listed in the declarations ` +
`of the NgModule '${ngModule.ref.node.name.text}', but is not a directive, a component, or a pipe. ` +
`Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`,
[{node: decl.node.name, messageText: `'${decl.node.name.text}' is declared here.`}]));
continue;
}
declared.add(decl.node);
}
// 3) process exports.
// Exports can contain modules, components, or directives. They're processed differently.
// Modules are straightforward. Directives and pipes from exported modules are added to the

View File

@ -598,6 +598,213 @@ runInEachFileSystem(os => {
expect(jsContents).toContain('outputs: { output: "output" }');
});
it('should pick a Pipe defined in `declarations` over imported Pipes', () => {
env.tsconfig({});
env.write('test.ts', `
import {Component, Pipe, NgModule} from '@angular/core';
// ModuleA classes
@Pipe({name: 'number'})
class PipeA {}
@NgModule({
declarations: [PipeA],
exports: [PipeA]
})
class ModuleA {}
// ModuleB classes
@Pipe({name: 'number'})
class PipeB {}
@Component({
selector: 'app',
template: '{{ count | number }}'
})
export class App {}
@NgModule({
imports: [ModuleA],
declarations: [PipeB, App],
})
class ModuleB {}
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain('pipes: [PipeB]');
});
it('should respect imported module order when selecting Pipe (last imported Pipe is used)',
() => {
env.tsconfig({});
env.write('test.ts', `
import {Component, Pipe, NgModule} from '@angular/core';
// ModuleA classes
@Pipe({name: 'number'})
class PipeA {}
@NgModule({
declarations: [PipeA],
exports: [PipeA]
})
class ModuleA {}
// ModuleB classes
@Pipe({name: 'number'})
class PipeB {}
@NgModule({
declarations: [PipeB],
exports: [PipeB]
})
class ModuleB {}
// ModuleC classes
@Component({
selector: 'app',
template: '{{ count | number }}'
})
export class App {}
@NgModule({
imports: [ModuleA, ModuleB],
declarations: [App],
})
class ModuleC {}
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain('pipes: [PipeB]');
});
it('should add Directives and Components from `declarations` at the end of the list', () => {
env.tsconfig({});
env.write('test.ts', `
import {Component, Directive, NgModule} from '@angular/core';
// ModuleA classes
@Directive({selector: '[dir]'})
class DirectiveA {}
@Component({
selector: 'comp',
template: '...'
})
class ComponentA {}
@NgModule({
declarations: [DirectiveA, ComponentA],
exports: [DirectiveA, ComponentA]
})
class ModuleA {}
// ModuleB classes
@Directive({selector: '[dir]'})
class DirectiveB {}
@Component({
selector: 'comp',
template: '...',
})
export class ComponentB {}
@Component({
selector: 'app',
template: \`
<div dir></div>
<comp></comp>
\`,
})
export class App {}
@NgModule({
imports: [ModuleA],
declarations: [DirectiveB, ComponentB, App],
})
class ModuleB {}
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain('directives: [DirectiveA, DirectiveB, ComponentA, ComponentB]');
});
it('should respect imported module order while processing Directives and Components', () => {
env.tsconfig({});
env.write('test.ts', `
import {Component, Directive, NgModule} from '@angular/core';
// ModuleA classes
@Directive({selector: '[dir]'})
class DirectiveA {}
@Component({
selector: 'comp',
template: '...'
})
class ComponentA {}
@NgModule({
declarations: [DirectiveA, ComponentA],
exports: [DirectiveA, ComponentA]
})
class ModuleA {}
// ModuleB classes
@Directive({selector: '[dir]'})
class DirectiveB {}
@Component({
selector: 'comp',
template: '...'
})
class ComponentB {}
@NgModule({
declarations: [DirectiveB, ComponentB],
exports: [DirectiveB, ComponentB]
})
class ModuleB {}
// ModuleC classes
@Component({
selector: 'app',
template: \`
<div dir></div>
<comp></comp>
\`,
})
export class App {}
@NgModule({
imports: [ModuleA, ModuleB],
declarations: [App],
})
class ModuleC {}
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain('directives: [DirectiveA, DirectiveB, ComponentA, ComponentB]');
});
it('should compile Components with a templateUrl in a different rootDir', () => {
env.tsconfig({}, ['./extraRootDir']);
env.write('extraRootDir/test.html', '<p>Hello World</p>');