feat(ivy): resolve forwardRef() for queries (#25080)

@ContentChild[ren] and @ViewChild[ren] can contain a forwardRef() to a
type. This commit allows ngtsc to unwrap the forward reference and
deal with the node inside.

It includes two modes of support for forward reference resolution -
a foreign function resolver which understands deeply nested forward
references in expressions that are being statically evaluated, and
an unwrapForwardRef() function which deals only with top-level nodes.

Both will be useful in the future, but for now only unwrapForwardRef()
is used.

PR Close #25080
This commit is contained in:
Alex Rickabaugh
2018-07-24 16:05:23 -07:00
committed by Igor Minar
parent 48d7205873
commit f902b5ec59
6 changed files with 122 additions and 23 deletions

View File

@ -14,7 +14,7 @@ import {Reference, filterToMembersWithDecorator, reflectObjectLiteral, staticall
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
@ -156,12 +156,13 @@ export function extractQueryMetadata(
throw new Error(`@${name} must have arguments`);
}
const first = name === 'ViewChild' || name === 'ContentChild';
const arg = staticallyResolve(args[0], reflector, checker);
const node = unwrapForwardRef(args[0], reflector);
const arg = staticallyResolve(node, reflector, checker);
// Extract the predicate
let predicate: Expression|string[]|null = null;
if (arg instanceof Reference) {
predicate = new WrappedNodeExpr(args[0]);
predicate = new WrappedNodeExpr(node);
} else if (typeof arg === 'string') {
predicate = [arg];
} else if (isStringArrayOrDie(arg, '@' + name)) {

View File

@ -67,14 +67,14 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (ngModule.has('imports')) {
const importsMeta = staticallyResolve(
ngModule.get('imports') !, this.reflector, this.checker,
node => this._extractModuleFromModuleWithProvidersFn(node));
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
imports = resolveTypeList(importsMeta, 'imports');
}
let exports: Reference[] = [];
if (ngModule.has('exports')) {
const exportsMeta = staticallyResolve(
ngModule.get('exports') !, this.reflector, this.checker,
node => this._extractModuleFromModuleWithProvidersFn(node));
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
exports = resolveTypeList(exportsMeta, 'exports');
}

View File

@ -10,7 +10,7 @@ import {Expression, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeE
import * as ts from 'typescript';
import {Decorator, ReflectionHost} from '../../host';
import {Reference} from '../../metadata';
import {AbsoluteReference, Reference} from '../../metadata';
export function getConstructorDependencies(
clazz: ts.ClassDeclaration, reflector: ReflectionHost,
@ -103,3 +103,69 @@ export function unwrapExpression(node: ts.Expression): ts.Expression {
}
return node;
}
function expandForwardRef(arg: ts.Expression): ts.Expression|null {
if (!ts.isArrowFunction(arg) && !ts.isFunctionExpression(arg)) {
return null;
}
const body = arg.body;
// Either the body is a ts.Expression directly, or a block with a single return statement.
if (ts.isBlock(body)) {
// Block body - look for a single return statement.
if (body.statements.length !== 1) {
return null;
}
const stmt = body.statements[0];
if (!ts.isReturnStatement(stmt) || stmt.expression === undefined) {
return null;
}
return stmt.expression;
} else {
// Shorthand body - return as an expression.
return body;
}
}
/**
* Possibly resolve a forwardRef() expression into the inner value.
*
* @param node the forwardRef() expression to resolve
* @param reflector a ReflectionHost
* @returns the resolved expression, if the original expression was a forwardRef(), or the original
* expression otherwise
*/
export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression {
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) ||
node.arguments.length !== 1) {
return node;
}
const expr = expandForwardRef(node.arguments[0]);
if (expr === null) {
return node;
}
const imp = reflector.getImportOfIdentifier(node.expression);
if (imp === null || imp.from !== '@angular/core' || imp.name !== 'forwardRef') {
return node;
} else {
return expr;
}
}
/**
* A foreign function resolver for `staticallyResolve` which unwraps forwardRef() expressions.
*
* @param ref a Reference to the declaration of the function being called (which might be
* forwardRef)
* @param args the arguments to the invocation of the forwardRef expression
* @returns an unwrapped argument if `ref` pointed to forwardRef, or null otherwise
*/
export function forwardRefResolver(
ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration>,
args: ts.Expression[]): ts.Expression|null {
if (!(ref instanceof AbsoluteReference) || ref.moduleName !== '@angular/core' ||
ref.symbolName !== 'forwardRef' || args.length !== 1) {
return null;
}
return expandForwardRef(args[0]);
}