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

@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
import {Component, Directive, ElementRef, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {Component, Directive, ElementRef, EventEmitter, NgModule, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {Input} from '@angular/core/src/metadata';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
@ -633,4 +633,96 @@ describe('directives', () => {
expect(div.getAttribute('title')).toBe('a');
});
});
describe('directives with the same selector', () => {
it('should process Directives from `declarations` list after imported ones', () => {
const log: string[] = [];
@Directive({selector: '[dir]'})
class DirectiveA {
constructor() { log.push('DirectiveA.constructor'); }
ngOnInit() { log.push('DirectiveA.ngOnInit'); }
}
@NgModule({
declarations: [DirectiveA],
exports: [DirectiveA],
})
class ModuleA {
}
@Directive({selector: '[dir]'})
class DirectiveB {
constructor() { log.push('DirectiveB.constructor'); }
ngOnInit() { log.push('DirectiveB.ngOnInit'); }
}
@Component({
selector: 'app',
template: '<div dir></div>',
})
class App {
}
TestBed.configureTestingModule({
imports: [ModuleA],
declarations: [DirectiveB, App],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(log).toEqual([
'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
'DirectiveB.ngOnInit'
]);
});
it('should respect imported module order', () => {
const log: string[] = [];
@Directive({selector: '[dir]'})
class DirectiveA {
constructor() { log.push('DirectiveA.constructor'); }
ngOnInit() { log.push('DirectiveA.ngOnInit'); }
}
@NgModule({
declarations: [DirectiveA],
exports: [DirectiveA],
})
class ModuleA {
}
@Directive({selector: '[dir]'})
class DirectiveB {
constructor() { log.push('DirectiveB.constructor'); }
ngOnInit() { log.push('DirectiveB.ngOnInit'); }
}
@NgModule({
declarations: [DirectiveB],
exports: [DirectiveB],
})
class ModuleB {
}
@Component({
selector: 'app',
template: '<div dir></div>',
})
class App {
}
TestBed.configureTestingModule({
imports: [ModuleA, ModuleB],
declarations: [App],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(log).toEqual([
'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
'DirectiveB.ngOnInit'
]);
});
});
});

View File

@ -110,6 +110,86 @@ describe('pipe', () => {
expect(fixture.nativeElement.textContent).toEqual('value a b default 0 1 2 3');
});
it('should pick a Pipe defined in `declarations` over imported Pipes', () => {
@Pipe({name: 'number'})
class PipeA implements PipeTransform {
transform(value: any) { return `PipeA: ${value}`; }
}
@NgModule({
declarations: [PipeA],
exports: [PipeA],
})
class ModuleA {
}
@Pipe({name: 'number'})
class PipeB implements PipeTransform {
transform(value: any) { return `PipeB: ${value}`; }
}
@Component({
selector: 'app',
template: '{{ count | number }}',
})
class App {
count = 10;
}
TestBed.configureTestingModule({
imports: [ModuleA],
declarations: [PipeB, App],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
});
it('should respect imported module order when selecting Pipe (last imported Pipe is used)',
() => {
@Pipe({name: 'number'})
class PipeA implements PipeTransform {
transform(value: any) { return `PipeA: ${value}`; }
}
@NgModule({
declarations: [PipeA],
exports: [PipeA],
})
class ModuleA {
}
@Pipe({name: 'number'})
class PipeB implements PipeTransform {
transform(value: any) { return `PipeB: ${value}`; }
}
@NgModule({
declarations: [PipeB],
exports: [PipeB],
})
class ModuleB {
}
@Component({
selector: 'app',
template: '{{ count | number }}',
})
class App {
count = 10;
}
TestBed.configureTestingModule({
imports: [ModuleA, ModuleB],
declarations: [App],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
});
it('should do nothing when no change', () => {
let calls: any[] = [];