angular/packages/compiler/src/injectable_compiler_2.ts
Pete Bacon Darwin 393efa54e6 fix(compiler): ensure that partially compiled queries can handle forward references (#44113)
When a partially compiled component or directive is "linked" in JIT mode, the body
of its declaration is evaluated by the JavaScript runtime. If a class is referenced
in a query (e.g. `ViewQuery` or `ContentQuery`) but its definition is later in the
file, then the reference must be wrapped in a `forwardRef()` call.

Previously, query predicates were not wrapped correctly in partial declarations
causing the code to crash at runtime. In AOT mode, this code is never evaluated
but instead transformed as part of the build, so this bug did not become apparent
until Angular Material started running JIT mode tests on its distributable output.

This change fixes this problem by noting when queries are wrapped in `forwardRef()`
calls and ensuring that this gets passed through to partial compilation declarations
and then suitably stripped during linking.

See https://github.com/angular/components/pull/23882 and https://github.com/angular/components/issues/23907

PR Close #44113
2021-11-10 18:25:16 +00:00

170 lines
6.1 KiB
TypeScript

/**
* @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 * as o from './output/output_ast';
import {compileFactoryFunction, FactoryTarget, R3DependencyMetadata, R3FactoryDelegateType, R3FactoryMetadata} from './render3/r3_factory';
import {Identifiers} from './render3/r3_identifiers';
import {convertFromMaybeForwardRefExpression, ForwardRefHandling, generateForwardRef, MaybeForwardRefExpression, R3CompiledExpression, R3Reference, typeWithParameters} from './render3/util';
import {DefinitionMap} from './render3/view/util';
export interface R3InjectableMetadata {
name: string;
type: R3Reference;
internalType: o.Expression;
typeArgumentCount: number;
providedIn: MaybeForwardRefExpression;
useClass?: MaybeForwardRefExpression;
useFactory?: o.Expression;
useExisting?: MaybeForwardRefExpression;
useValue?: MaybeForwardRefExpression;
deps?: R3DependencyMetadata[];
}
export function compileInjectable(
meta: R3InjectableMetadata, resolveForwardRefs: boolean): R3CompiledExpression {
let result: {expression: o.Expression, statements: o.Statement[]}|null = null;
const factoryMeta: R3FactoryMetadata = {
name: meta.name,
type: meta.type,
internalType: meta.internalType,
typeArgumentCount: meta.typeArgumentCount,
deps: [],
target: FactoryTarget.Injectable,
};
if (meta.useClass !== undefined) {
// meta.useClass has two modes of operation. Either deps are specified, in which case `new` is
// used to instantiate the class with dependencies injected, or deps are not specified and
// the factory of the class is used to instantiate it.
//
// A special case exists for useClass: Type where Type is the injectable type itself and no
// deps are specified, in which case 'useClass' is effectively ignored.
const useClassOnSelf = meta.useClass.expression.isEquivalent(meta.internalType);
let deps: R3DependencyMetadata[]|undefined = undefined;
if (meta.deps !== undefined) {
deps = meta.deps;
}
if (deps !== undefined) {
// factory: () => new meta.useClass(...deps)
result = compileFactoryFunction({
...factoryMeta,
delegate: meta.useClass.expression,
delegateDeps: deps,
delegateType: R3FactoryDelegateType.Class,
});
} else if (useClassOnSelf) {
result = compileFactoryFunction(factoryMeta);
} else {
result = {
statements: [],
expression: delegateToFactory(
meta.type.value as o.WrappedNodeExpr<any>,
meta.useClass.expression as o.WrappedNodeExpr<any>, resolveForwardRefs)
};
}
} else if (meta.useFactory !== undefined) {
if (meta.deps !== undefined) {
result = compileFactoryFunction({
...factoryMeta,
delegate: meta.useFactory,
delegateDeps: meta.deps || [],
delegateType: R3FactoryDelegateType.Function,
});
} else {
result = {
statements: [],
expression: o.fn([], [new o.ReturnStatement(meta.useFactory.callFn([]))])
};
}
} else if (meta.useValue !== undefined) {
// Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for
// client code because meta.useValue is an Expression which will be defined even if the actual
// value is undefined.
result = compileFactoryFunction({
...factoryMeta,
expression: meta.useValue.expression,
});
} else if (meta.useExisting !== undefined) {
// useExisting is an `inject` call on the existing token.
result = compileFactoryFunction({
...factoryMeta,
expression: o.importExpr(Identifiers.inject).callFn([meta.useExisting.expression]),
});
} else {
result = {
statements: [],
expression: delegateToFactory(
meta.type.value as o.WrappedNodeExpr<any>, meta.internalType as o.WrappedNodeExpr<any>,
resolveForwardRefs)
};
}
const token = meta.internalType;
const injectableProps =
new DefinitionMap<{token: o.Expression, factory: o.Expression, providedIn: o.Expression}>();
injectableProps.set('token', token);
injectableProps.set('factory', result.expression);
// Only generate providedIn property if it has a non-null value
if ((meta.providedIn.expression as o.LiteralExpr).value !== null) {
injectableProps.set('providedIn', convertFromMaybeForwardRefExpression(meta.providedIn));
}
const expression = o.importExpr(Identifiers.ɵɵdefineInjectable)
.callFn([injectableProps.toLiteralMap()], undefined, true);
return {
expression,
type: createInjectableType(meta),
statements: result.statements,
};
}
export function createInjectableType(meta: R3InjectableMetadata) {
return new o.ExpressionType(o.importExpr(
Identifiers.InjectableDeclaration,
[typeWithParameters(meta.type.type, meta.typeArgumentCount)]));
}
function delegateToFactory(
type: o.WrappedNodeExpr<any>, internalType: o.WrappedNodeExpr<any>,
unwrapForwardRefs: boolean): o.Expression {
if (type.node === internalType.node) {
// The types are the same, so we can simply delegate directly to the type's factory.
// ```
// factory: type.ɵfac
// ```
return internalType.prop('ɵfac');
}
if (!unwrapForwardRefs) {
// The type is not wrapped in a `forwardRef()`, so we create a simple factory function that
// accepts a sub-type as an argument.
// ```
// factory: function(t) { return internalType.ɵfac(t); }
// ```
return createFactoryFunction(internalType);
}
// The internalType is actually wrapped in a `forwardRef()` so we need to resolve that before
// calling its factory.
// ```
// factory: function(t) { return core.resolveForwardRef(type).ɵfac(t); }
// ```
const unwrappedType = o.importExpr(Identifiers.resolveForwardRef).callFn([internalType]);
return createFactoryFunction(unwrappedType);
}
function createFactoryFunction(type: o.Expression): o.FunctionExpr {
return o.fn(
[new o.FnParam('t', o.DYNAMIC_TYPE)],
[new o.ReturnStatement(type.prop('ɵfac').callFn([o.variable('t')]))]);
}