diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts index 9bdeadc4f80..4d5b6bc5cf4 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts @@ -6,16 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileInjector, compileNgModule, CUSTOM_ELEMENTS_SCHEMA, Expression, ExternalExpr, FactoryTarget, InvokeFunctionExpr, LiteralArrayExpr, NO_ERRORS_SCHEMA, R3ClassMetadata, R3CompiledExpression, R3FactoryMetadata, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, R3SelectorScopeMode, SchemaMetadata, Statement, WrappedNodeExpr} from '@angular/compiler'; +import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileInjector, compileNgModule, Expression, ExternalExpr, FactoryTarget, InvokeFunctionExpr, LiteralArrayExpr, R3ClassMetadata, R3CompiledExpression, R3FactoryMetadata, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, R3SelectorScopeMode, SchemaMetadata, Statement, WrappedNodeExpr} from '@angular/compiler'; +import {createModuleWithProvidersResolver, isResolvedModuleWithProviders} from '@angular/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; import {assertSuccessfulReferenceEmit, Reference, ReferenceEmitter} from '../../../imports'; import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../../incremental/semantic_graph'; import {InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata'; -import {DynamicValue, PartialEvaluator, ResolvedValue, SyntheticValue} from '../../../partial_evaluator'; +import {PartialEvaluator, ResolvedValue, SyntheticValue} from '../../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../../perf'; -import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../../reflection'; +import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; import {LocalModuleScopeRegistry, ScopeData} from '../../../scope'; import {getDiagnosticNode} from '../../../scope/src/util'; import {FactoryTracker} from '../../../shims/api'; @@ -179,8 +180,7 @@ export class NgModuleDecoratorHandler implements } const moduleResolvers = combineResolvers([ - (fn, call, resolve, unresolvable) => - this._extractModuleFromModuleWithProvidersFn(fn, call, resolve, unresolvable), + createModuleWithProvidersResolver(this.reflector, this.isCore), forwardRefResolver, ]); @@ -644,112 +644,6 @@ export class NgModuleDecoratorHandler implements } } - /** - * Given a `FunctionDeclaration`, `MethodDeclaration` or `FunctionExpression`, check if it is - * typed as a `ModuleWithProviders` and return an expression referencing the module if available. - */ - private _extractModuleFromModuleWithProvidersFn( - fn: Reference, - node: ts.CallExpression, resolve: (expr: ts.Expression) => ResolvedValue, - unresolvable: DynamicValue): SyntheticValue|DynamicValue { - const rawType = fn.node.type || null; - - const type = rawType && - (this._reflectModuleFromTypeParam(rawType, fn.node) || - this._reflectModuleFromLiteralType(rawType)); - if (type === null) { - return unresolvable; - } - const ngModule = resolve(type); - if (!(ngModule instanceof Reference) || !isNamedClassDeclaration(ngModule.node)) { - return unresolvable; - } - - return new SyntheticValue({ - ngModule: ngModule as Reference, - mwpCall: node, - }); - } - - /** - * Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form: - * `ModuleWithProviders` - * @param type The type to reflect on. - * @returns the identifier of the NgModule type if found, or null otherwise. - */ - private _reflectModuleFromTypeParam( - type: ts.TypeNode, - node: ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression): ts.Expression|null { - // Examine the type of the function to see if it's a ModuleWithProviders reference. - if (!ts.isTypeReferenceNode(type)) { - return null; - } - - const typeName = type && - (ts.isIdentifier(type.typeName) && type.typeName || - ts.isQualifiedName(type.typeName) && type.typeName.right) || - null; - if (typeName === null) { - return null; - } - - // Look at the type itself to see where it comes from. - const id = this.reflector.getImportOfIdentifier(typeName); - - // If it's not named ModuleWithProviders, bail. - if (id === null || id.name !== 'ModuleWithProviders') { - return null; - } - - // If it's not from @angular/core, bail. - if (!this.isCore && id.from !== '@angular/core') { - return null; - } - - // If there's no type parameter specified, bail. - if (type.typeArguments === undefined || type.typeArguments.length !== 1) { - const parent = - ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) ? node.parent : null; - const symbolName = (parent && parent.name ? parent.name.getText() + '.' : '') + - (node.name ? node.name.getText() : 'anonymous'); - throw new FatalDiagnosticError( - ErrorCode.NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC, type, - `${symbolName} returns a ModuleWithProviders type without a generic type argument. ` + - `Please add a generic type argument to the ModuleWithProviders type. If this ` + - `occurrence is in library code you don't control, please contact the library authors.`); - } - - const arg = type.typeArguments[0]; - - return typeNodeToValueExpr(arg); - } - - /** - * Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form: - * `A|B|{ngModule: T}|C`. - * @param type The type to reflect on. - * @returns the identifier of the NgModule type if found, or null otherwise. - */ - private _reflectModuleFromLiteralType(type: ts.TypeNode): ts.Expression|null { - if (!ts.isIntersectionTypeNode(type)) { - return null; - } - for (const t of type.types) { - if (ts.isTypeLiteralNode(t)) { - for (const m of t.members) { - const ngModuleType = ts.isPropertySignature(m) && ts.isIdentifier(m.name) && - m.name.text === 'ngModule' && m.type || - null; - const ngModuleExpression = ngModuleType && typeNodeToValueExpr(ngModuleType); - if (ngModuleExpression) { - return ngModuleExpression; - } - } - } - } - return null; - } - // Verify that a "Declaration" reference is a `ClassDeclaration` reference. private isClassDeclarationReference(ref: Reference): ref is Reference { return this.reflector.isClass(ref.node); @@ -826,24 +720,12 @@ function isModuleIdExpression(expr: ts.Expression): boolean { expr.expression.text === 'module' && expr.name.text === 'id'; } -interface ResolvedModuleWithProviders { - ngModule: Reference; - mwpCall: ts.CallExpression; -} - export interface TopLevelImportedExpression { expression: ts.Expression; resolvedReferences: Array>; hasModuleWithProviders: boolean; } -function isResolvedModuleWithProviders(sv: SyntheticValue): - sv is SyntheticValue { - return typeof sv.value === 'object' && sv.value != null && - sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) && - sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders); -} - /** * Helper method to produce a diagnostics for a situation when a standalone component * is referenced in the `@NgModule.bootstrap` array. diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts new file mode 100644 index 00000000000..e1fcd44d3fd --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright Google LLC 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 ts from 'typescript'; + +import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; +import {Reference} from '../../../imports'; +import {ForeignFunctionResolver, SyntheticValue} from '../../../partial_evaluator'; +import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost, typeNodeToValueExpr} from '../../../reflection'; + +/** + * Creates a foreign function resolver to detect a `ModuleWithProviders` type in a return type + * position of a function or method declaration. A `SyntheticValue` is produced if such a return + * type is recognized. + * + * @param reflector The reflection host to use for analyzing the syntax. + * @param isCore Whether the @angular/core package is being compiled. + */ +export function createModuleWithProvidersResolver( + reflector: ReflectionHost, isCore: boolean): ForeignFunctionResolver { + /** + * Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form: + * `ModuleWithProviders` + * @param type The type to reflect on. + * @returns the identifier of the NgModule type if found, or null otherwise. + */ + function _reflectModuleFromTypeParam( + type: ts.TypeNode, + node: ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression): ts.Expression|null { + // Examine the type of the function to see if it's a ModuleWithProviders reference. + if (!ts.isTypeReferenceNode(type)) { + return null; + } + + const typeName = type && + (ts.isIdentifier(type.typeName) && type.typeName || + ts.isQualifiedName(type.typeName) && type.typeName.right) || + null; + if (typeName === null) { + return null; + } + + // Look at the type itself to see where it comes from. + const id = reflector.getImportOfIdentifier(typeName); + + // If it's not named ModuleWithProviders, bail. + if (id === null || id.name !== 'ModuleWithProviders') { + return null; + } + + // If it's not from @angular/core, bail. + if (!isCore && id.from !== '@angular/core') { + return null; + } + + // If there's no type parameter specified, bail. + if (type.typeArguments === undefined || type.typeArguments.length !== 1) { + const parent = + ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) ? node.parent : null; + const symbolName = (parent && parent.name ? parent.name.getText() + '.' : '') + + (node.name ? node.name.getText() : 'anonymous'); + throw new FatalDiagnosticError( + ErrorCode.NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC, type, + `${symbolName} returns a ModuleWithProviders type without a generic type argument. ` + + `Please add a generic type argument to the ModuleWithProviders type. If this ` + + `occurrence is in library code you don't control, please contact the library authors.`); + } + + const arg = type.typeArguments[0]; + + return typeNodeToValueExpr(arg); + } + + /** + * Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form: + * `A|B|{ngModule: T}|C`. + * @param type The type to reflect on. + * @returns the identifier of the NgModule type if found, or null otherwise. + */ + function _reflectModuleFromLiteralType(type: ts.TypeNode): ts.Expression|null { + if (!ts.isIntersectionTypeNode(type)) { + return null; + } + for (const t of type.types) { + if (ts.isTypeLiteralNode(t)) { + for (const m of t.members) { + const ngModuleType = ts.isPropertySignature(m) && ts.isIdentifier(m.name) && + m.name.text === 'ngModule' && m.type || + null; + const ngModuleExpression = ngModuleType && typeNodeToValueExpr(ngModuleType); + if (ngModuleExpression) { + return ngModuleExpression; + } + } + } + } + return null; + } + + return (fn, callExpr, resolve, unresolvable) => { + const rawType = fn.node.type; + if (rawType === undefined) { + return unresolvable; + } + + const type = + _reflectModuleFromTypeParam(rawType, fn.node) ?? _reflectModuleFromLiteralType(rawType); + if (type === null) { + return unresolvable; + } + const ngModule = resolve(type); + if (!(ngModule instanceof Reference) || !isNamedClassDeclaration(ngModule.node)) { + return unresolvable; + } + + return new SyntheticValue({ + ngModule: ngModule as Reference, + mwpCall: callExpr, + }); + }; +} + +export interface ResolvedModuleWithProviders { + ngModule: Reference; + mwpCall: ts.CallExpression; +} + +export function isResolvedModuleWithProviders(sv: SyntheticValue): + sv is SyntheticValue { + return typeof sv.value === 'object' && sv.value != null && + sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) && + sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders); +}