Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6f2fd6e

Browse files
AndrewKushnirmatsko
authored andcommitted
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
1 parent 8a68a7c commit 6f2fd6e

File tree

5 files changed

+422
-42
lines changed

5 files changed

+422
-42
lines changed

‎packages/compiler-cli/src/ngtsc/scope/src/local.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
296296
const exportPipes = new Map<ts.Declaration, PipeMeta>();
297297

298298
// The algorithm is as follows:
299-
// 1) Add directives/pipes declared in the NgModule to the compilation scope.
300-
// 2) Add all of the directives/pipes from each NgModule imported into the current one to the
301-
// compilation scope. At this point, the compilation scope is complete.
299+
// 1) Add all of the directives/pipes from each NgModule imported into the current one to the
300+
// compilation scope.
301+
// 2) Add directives/pipes declared in the NgModule to the compilation scope. At this point, the
302+
// compilation scope is complete.
302303
// 3) For each entry in the NgModule's exports:
303304
// a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is
304305
// one, add them to the export scope of this NgModule.
@@ -307,31 +308,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
307308
// c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this
308309
// is an error.
309310

310-
// 1) add declarations.
311-
for (const decl of ngModule.declarations) {
312-
const directive = this.localReader.getDirectiveMetadata(decl);
313-
const pipe = this.localReader.getPipeMetadata(decl);
314-
if (directive !== null) {
315-
compilationDirectives.set(decl.node, {...directive, ref: decl});
316-
} else if (pipe !== null) {
317-
compilationPipes.set(decl.node, {...pipe, ref: decl});
318-
} else {
319-
this.taintedModules.add(ngModule.ref.node);
320-
321-
const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations !);
322-
diagnostics.push(makeDiagnostic(
323-
ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode,
324-
`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.
325-
326-
Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`,
327-
[{node: decl.node.name, messageText: `'${decl.node.name.text}' is declared here.`}]));
328-
continue;
329-
}
330-
331-
declared.add(decl.node);
332-
}
333-
334-
// 2) process imports.
311+
// 1) process imports.
335312
for (const decl of ngModule.imports) {
336313
const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'import');
337314
if (importScope === null) {
@@ -353,6 +330,30 @@ Either remove it from the NgModule's declarations, or add an appropriate Angular
353330
}
354331
}
355332

333+
// 2) add declarations.
334+
for (const decl of ngModule.declarations) {
335+
const directive = this.localReader.getDirectiveMetadata(decl);
336+
const pipe = this.localReader.getPipeMetadata(decl);
337+
if (directive !== null) {
338+
compilationDirectives.set(decl.node, {...directive, ref: decl});
339+
} else if (pipe !== null) {
340+
compilationPipes.set(decl.node, {...pipe, ref: decl});
341+
} else {
342+
this.taintedModules.add(ngModule.ref.node);
343+
344+
const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations !);
345+
diagnostics.push(makeDiagnostic(
346+
ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode,
347+
`The class '${decl.node.name.text}' is listed in the declarations ` +
348+
`of the NgModule '${ngModule.ref.node.name.text}', but is not a directive, a component, or a pipe. ` +
349+
`Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`,
350+
[{node: decl.node.name, messageText: `'${decl.node.name.text}' is declared here.`}]));
351+
continue;
352+
}
353+
354+
declared.add(decl.node);
355+
}
356+
356357
// 3) process exports.
357358
// Exports can contain modules, components, or directives. They're processed differently.
358359
// Modules are straightforward. Directives and pipes from exported modules are added to the

‎packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,213 @@ runInEachFileSystem(os => {
598598
expect(jsContents).toContain('outputs: { output: "output" }');
599599
});
600600

601+
it('should pick a Pipe defined in `declarations` over imported Pipes', () => {
602+
env.tsconfig({});
603+
env.write('test.ts', `
604+
import {Component, Pipe, NgModule} from '@angular/core';
605+
606+
// ModuleA classes
607+
608+
@Pipe({name: 'number'})
609+
class PipeA {}
610+
611+
@NgModule({
612+
declarations: [PipeA],
613+
exports: [PipeA]
614+
})
615+
class ModuleA {}
616+
617+
// ModuleB classes
618+
619+
@Pipe({name: 'number'})
620+
class PipeB {}
621+
622+
@Component({
623+
selector: 'app',
624+
template: '{{ count | number }}'
625+
})
626+
export class App {}
627+
628+
@NgModule({
629+
imports: [ModuleA],
630+
declarations: [PipeB, App],
631+
})
632+
class ModuleB {}
633+
`);
634+
635+
env.driveMain();
636+
637+
const jsContents = trim(env.getContents('test.js'));
638+
expect(jsContents).toContain('pipes: [PipeB]');
639+
});
640+
641+
it('should respect imported module order when selecting Pipe (last imported Pipe is used)',
642+
() => {
643+
env.tsconfig({});
644+
env.write('test.ts', `
645+
import {Component, Pipe, NgModule} from '@angular/core';
646+
647+
// ModuleA classes
648+
649+
@Pipe({name: 'number'})
650+
class PipeA {}
651+
652+
@NgModule({
653+
declarations: [PipeA],
654+
exports: [PipeA]
655+
})
656+
class ModuleA {}
657+
658+
// ModuleB classes
659+
660+
@Pipe({name: 'number'})
661+
class PipeB {}
662+
663+
@NgModule({
664+
declarations: [PipeB],
665+
exports: [PipeB]
666+
})
667+
class ModuleB {}
668+
669+
// ModuleC classes
670+
671+
@Component({
672+
selector: 'app',
673+
template: '{{ count | number }}'
674+
})
675+
export class App {}
676+
677+
@NgModule({
678+
imports: [ModuleA, ModuleB],
679+
declarations: [App],
680+
})
681+
class ModuleC {}
682+
`);
683+
684+
env.driveMain();
685+
686+
const jsContents = trim(env.getContents('test.js'));
687+
expect(jsContents).toContain('pipes: [PipeB]');
688+
});
689+
690+
it('should add Directives and Components from `declarations` at the end of the list', () => {
691+
env.tsconfig({});
692+
env.write('test.ts', `
693+
import {Component, Directive, NgModule} from '@angular/core';
694+
695+
// ModuleA classes
696+
697+
@Directive({selector: '[dir]'})
698+
class DirectiveA {}
699+
700+
@Component({
701+
selector: 'comp',
702+
template: '...'
703+
})
704+
class ComponentA {}
705+
706+
@NgModule({
707+
declarations: [DirectiveA, ComponentA],
708+
exports: [DirectiveA, ComponentA]
709+
})
710+
class ModuleA {}
711+
712+
// ModuleB classes
713+
714+
@Directive({selector: '[dir]'})
715+
class DirectiveB {}
716+
717+
@Component({
718+
selector: 'comp',
719+
template: '...',
720+
})
721+
export class ComponentB {}
722+
723+
@Component({
724+
selector: 'app',
725+
template: \`
726+
<div dir></div>
727+
<comp></comp>
728+
\`,
729+
})
730+
export class App {}
731+
732+
@NgModule({
733+
imports: [ModuleA],
734+
declarations: [DirectiveB, ComponentB, App],
735+
})
736+
class ModuleB {}
737+
`);
738+
739+
env.driveMain();
740+
741+
const jsContents = trim(env.getContents('test.js'));
742+
expect(jsContents).toContain('directives: [DirectiveA, DirectiveB, ComponentA, ComponentB]');
743+
});
744+
745+
it('should respect imported module order while processing Directives and Components', () => {
746+
env.tsconfig({});
747+
env.write('test.ts', `
748+
import {Component, Directive, NgModule} from '@angular/core';
749+
750+
// ModuleA classes
751+
752+
@Directive({selector: '[dir]'})
753+
class DirectiveA {}
754+
755+
@Component({
756+
selector: 'comp',
757+
template: '...'
758+
})
759+
class ComponentA {}
760+
761+
@NgModule({
762+
declarations: [DirectiveA, ComponentA],
763+
exports: [DirectiveA, ComponentA]
764+
})
765+
class ModuleA {}
766+
767+
// ModuleB classes
768+
769+
@Directive({selector: '[dir]'})
770+
class DirectiveB {}
771+
772+
@Component({
773+
selector: 'comp',
774+
template: '...'
775+
})
776+
class ComponentB {}
777+
778+
@NgModule({
779+
declarations: [DirectiveB, ComponentB],
780+
exports: [DirectiveB, ComponentB]
781+
})
782+
class ModuleB {}
783+
784+
// ModuleC classes
785+
786+
@Component({
787+
selector: 'app',
788+
template: \`
789+
<div dir></div>
790+
<comp></comp>
791+
\`,
792+
})
793+
export class App {}
794+
795+
@NgModule({
796+
imports: [ModuleA, ModuleB],
797+
declarations: [App],
798+
})
799+
class ModuleC {}
800+
`);
801+
802+
env.driveMain();
803+
804+
const jsContents = trim(env.getContents('test.js'));
805+
expect(jsContents).toContain('directives: [DirectiveA, DirectiveB, ComponentA, ComponentB]');
806+
});
807+
601808
it('should compile Components with a templateUrl in a different rootDir', () => {
602809
env.tsconfig({}, ['./extraRootDir']);
603810
env.write('extraRootDir/test.html', '<p>Hello World</p>');

‎packages/core/src/render3/jit/module.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -455,19 +455,6 @@ export function transitiveScopesFor<T>(moduleType: Type<T>): NgModuleTransitiveS
455455
},
456456
};
457457

458-
maybeUnwrapFn(def.declarations).forEach(declared => {
459-
const declaredWithDefs = declared as Type<any>& { ɵpipe?: any; };
460-
461-
if (getPipeDef(declaredWithDefs)) {
462-
scopes.compilation.pipes.add(declared);
463-
} else {
464-
// Either declared has a ɵcmp or ɵdir, or it's a component which hasn't
465-
// had its template compiled yet. In either case, it gets added to the compilation's
466-
// directives.
467-
scopes.compilation.directives.add(declared);
468-
}
469-
});
470-
471458
maybeUnwrapFn(def.imports).forEach(<I>(imported: Type<I>) => {
472459
const importedType = imported as Type<I>& {
473460
// If imported is an @NgModule:
@@ -485,6 +472,19 @@ export function transitiveScopesFor<T>(moduleType: Type<T>): NgModuleTransitiveS
485472
importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry));
486473
});
487474

475+
maybeUnwrapFn(def.declarations).forEach(declared => {
476+
const declaredWithDefs = declared as Type<any>& { ɵpipe?: any; };
477+
478+
if (getPipeDef(declaredWithDefs)) {
479+
scopes.compilation.pipes.add(declared);
480+
} else {
481+
// Either declared has a ɵcmp or ɵdir, or it's a component which hasn't
482+
// had its template compiled yet. In either case, it gets added to the compilation's
483+
// directives.
484+
scopes.compilation.directives.add(declared);
485+
}
486+
});
487+
488488
maybeUnwrapFn(def.exports).forEach(<E>(exported: Type<E>) => {
489489
const exportedType = exported as Type<E>& {
490490
// Components, Directives, NgModules, and Pipes can all be exported.

0 commit comments

Comments
 (0)