From efa10e33a9d1281cc8aeb2b004f3bb60f50cafbb Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 27 Feb 2019 19:33:18 +0100 Subject: [PATCH] fix(ivy): resolve forwardRef when analyzing NgModule (#28942) Fixes forward refs not being resolved when an NgModule is being analyzed by ngtsc. This PR resolves FW-1094. PR Close #28942 --- .../src/ngtsc/annotations/src/ng_module.ts | 17 ++-- .../src/ngtsc/annotations/src/util.ts | 19 +++++ .../ngtsc/annotations/test/ng_module_spec.ts | 82 +++++++++++++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index de2ae6649b..0dce76e968 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -20,7 +20,7 @@ import {getSourceFile} from '../../util/src/typescript'; import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; -import {getValidConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util'; +import {combineResolvers, forwardRefResolver, getValidConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util'; export interface NgModuleAnalysis { ngModuleDef: R3NgModuleMetadata; @@ -83,34 +83,37 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(ref.node), + forwardRefResolver, + ]); + // Extract the module declarations, imports, and exports. let declarations: Reference[] = []; if (ngModule.has('declarations')) { const expr = ngModule.get('declarations') !; - const declarationMeta = this.evaluator.evaluate(expr); + const declarationMeta = this.evaluator.evaluate(expr, forwardRefResolver); declarations = this.resolveTypeList(expr, declarationMeta, 'declarations'); } let imports: Reference[] = []; let rawImports: ts.Expression|null = null; if (ngModule.has('imports')) { rawImports = ngModule.get('imports') !; - const importsMeta = this.evaluator.evaluate( - rawImports, ref => this._extractModuleFromModuleWithProvidersFn(ref.node)); + const importsMeta = this.evaluator.evaluate(rawImports, moduleResolvers); imports = this.resolveTypeList(rawImports, importsMeta, 'imports'); } let exports: Reference[] = []; let rawExports: ts.Expression|null = null; if (ngModule.has('exports')) { rawExports = ngModule.get('exports') !; - const exportsMeta = this.evaluator.evaluate( - rawExports, ref => this._extractModuleFromModuleWithProvidersFn(ref.node)); + const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers); exports = this.resolveTypeList(rawExports, exportsMeta, 'exports'); this.referencesRegistry.add(node, ...exports); } let bootstrap: Reference[] = []; if (ngModule.has('bootstrap')) { const expr = ngModule.get('bootstrap') !; - const bootstrapMeta = this.evaluator.evaluate(expr); + const bootstrapMeta = this.evaluator.evaluate(expr, forwardRefResolver); bootstrap = this.resolveTypeList(expr, bootstrapMeta, 'bootstrap'); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 0dc41be777..002a5803d7 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ImportMode, Reference, ReferenceEmitter} from '../../imports'; +import {ForeignFunctionResolver} from '../../partial_evaluator'; import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection'; export enum ConstructorDepErrorKind { @@ -213,3 +214,21 @@ export function forwardRefResolver( } return expandForwardRef(args[0]); } + +/** + * Combines an array of resolver functions into a one. + * @param resolvers Resolvers to be combined. + */ +export function combineResolvers(resolvers: ForeignFunctionResolver[]): ForeignFunctionResolver { + return (ref: Reference, + args: ts.Expression[]): ts.Expression | + null => { + for (const resolver of resolvers) { + const resolved = resolver(ref, args); + if (resolved !== null) { + return resolved; + } + } + return null; + }; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts new file mode 100644 index 0000000000..1464e81f45 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {WrappedNodeExpr} from '@angular/compiler'; +import {R3Reference} from '@angular/compiler/src/compiler'; +import * as ts from 'typescript'; + +import {LocalIdentifierStrategy, ReferenceEmitter} from '../../imports'; +import {PartialEvaluator} from '../../partial_evaluator'; +import {TypeScriptReflectionHost} from '../../reflection'; +import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; +import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {NgModuleDecoratorHandler} from '../src/ng_module'; +import {NoopReferencesRegistry} from '../src/references_registry'; + +describe('NgModuleDecoratorHandler', () => { + it('should resolve forwardRef', () => { + const {program} = makeProgram([ + { + name: 'node_modules/@angular/core/index.d.ts', + contents: ` + export const Component: any; + export const NgModule: any; + export declare function forwardRef(fn: () => any): any; + `, + }, + { + name: 'entry.ts', + contents: ` + import {Component, forwardRef, NgModule} from '@angular/core'; + + @Component({ + template: '', + }) + export class TestComp {} + + @NgModule() + export class TestModuleDependency {} + + @NgModule({ + declarations: [forwardRef(() => TestComp)], + exports: [forwardRef(() => TestComp)], + imports: [forwardRef(() => TestModuleDependency)] + }) + export class TestModule {} + ` + }, + ]); + const checker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker); + const referencesRegistry = new NoopReferencesRegistry(); + const scopeRegistry = new LocalModuleScopeRegistry( + new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]), + null); + const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); + + const handler = new NgModuleDecoratorHandler( + reflectionHost, evaluator, scopeRegistry, referencesRegistry, false, null, refEmitter); + const TestModule = getDeclaration(program, 'entry.ts', 'TestModule', ts.isClassDeclaration); + const detected = + handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); + if (detected === undefined) { + return fail('Failed to recognize @NgModule'); + } + const moduleDef = handler.analyze(TestModule, detected.metadata).analysis !.ngModuleDef; + + expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']); + expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']); + expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']); + + function getReferenceIdentifierTexts(references: R3Reference[]) { + return references.map(ref => (ref.value as WrappedNodeExpr).node.text); + } + }); + +});