refactor(compiler-cli): extract ModuleWithProviders analysis out of NgModule handler (#46009)

This commit moves the foreign function resolver logic for detecting a
`ModuleWithProviders` in a return type position of a function call, as the logic can
then be reused for standalone components in a subsequent commit.

PR Close #46009
This commit is contained in:
JoostK 2022-05-16 19:38:04 +02:00 committed by Alex Rickabaugh
parent a99f2eb38d
commit 3d19f32942
2 changed files with 143 additions and 123 deletions

View file

@ -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<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
node: ts.CallExpression, resolve: (expr: ts.Expression) => ResolvedValue,
unresolvable: DynamicValue): SyntheticValue<ResolvedModuleWithProviders>|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<ClassDeclaration>,
mwpCall: node,
});
}
/**
* Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form:
* `ModuleWithProviders<T>`
* @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<ClassDeclaration> {
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<ClassDeclaration>;
mwpCall: ts.CallExpression;
}
export interface TopLevelImportedExpression {
expression: ts.Expression;
resolvedReferences: Array<Reference<ClassDeclaration>>;
hasModuleWithProviders: boolean;
}
function isResolvedModuleWithProviders(sv: SyntheticValue<unknown>):
sv is SyntheticValue<ResolvedModuleWithProviders> {
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.

View file

@ -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<T>` 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<T>`
* @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<ClassDeclaration>,
mwpCall: callExpr,
});
};
}
export interface ResolvedModuleWithProviders {
ngModule: Reference<ClassDeclaration>;
mwpCall: ts.CallExpression;
}
export function isResolvedModuleWithProviders(sv: SyntheticValue<unknown>):
sv is SyntheticValue<ResolvedModuleWithProviders> {
return typeof sv.value === 'object' && sv.value != null &&
sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) &&
sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders);
}