Alex Rickabaugh 13c2fad240 fix(ivy): throw a better error when DI can't inject a ctor param (#33739)
Occasionally a factory function needs to be generated for an "invalid"
constructor (one with parameters types which aren't injectable). Typically
this happens in JIT mode where understanding of parameters cannot be done in
the same "up-front" way that the AOT compiler can.

This commit changes the JIT compiler to generate a new `invalidFactoryDep`
call for each invalid parameter. This instruction will error at runtime if
called, indicating both the index of the invalid parameter as well as (via
the stack trace) the factory function which was generated for the type being
constructed.

Fixes #33637

PR Close #33739
2019-12-09 11:37:10 -08:00

373 lines
13 KiB
TypeScript

/**
* @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 {StaticSymbol} from '../aot/static_symbol';
import {CompileTypeMetadata, tokenReference} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector';
import {InjectFlags} from '../core';
import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast';
import {Identifiers as R3} from '../render3/r3_identifiers';
import {OutputContext} from '../util';
import {typeWithParameters} from './util';
import {unsupported} from './view/util';
/**
* Metadata required by the factory generator to generate a `factory` function for a type.
*/
export interface R3ConstructorFactoryMetadata {
/**
* String name of the type being generated (used to name the factory function).
*/
name: string;
/**
* An expression representing the interface type being constructed.
*/
type: o.Expression;
/**
* An expression representing the constructor type, intended for use within a class definition
* itself.
*
* This can differ from the outer `type` if the class is being compiled by ngcc and is inside
* an IIFE structure that uses a different name internally.
*/
internalType: o.Expression;
/** Number of arguments for the `type`. */
typeArgumentCount: number;
/**
* Regardless of whether `fnOrClass` is a constructor function or a user-defined factory, it
* may have 0 or more parameters, which will be injected according to the `R3DependencyMetadata`
* for those parameters. If this is `null`, then the type's constructor is nonexistent and will
* be inherited from `fnOrClass` which is interpreted as the current type. If this is `'invalid'`,
* then one or more of the parameters wasn't resolvable and any attempt to use these deps will
* result in a runtime error.
*/
deps: R3DependencyMetadata[]|'invalid'|null;
/**
* An expression for the function which will be used to inject dependencies. The API of this
* function could be different, and other options control how it will be invoked.
*/
injectFn: o.ExternalReference;
/**
* Type of the target being created by the factory.
*/
target: R3FactoryTarget;
}
export enum R3FactoryDelegateType {
Class,
Function,
Factory,
}
export interface R3DelegatedFactoryMetadata extends R3ConstructorFactoryMetadata {
delegate: o.Expression;
delegateType: R3FactoryDelegateType.Factory;
}
export interface R3DelegatedFnOrClassMetadata extends R3ConstructorFactoryMetadata {
delegate: o.Expression;
delegateType: R3FactoryDelegateType.Class|R3FactoryDelegateType.Function;
delegateDeps: R3DependencyMetadata[];
}
export interface R3ExpressionFactoryMetadata extends R3ConstructorFactoryMetadata {
expression: o.Expression;
}
export type R3FactoryMetadata = R3ConstructorFactoryMetadata | R3DelegatedFactoryMetadata |
R3DelegatedFnOrClassMetadata | R3ExpressionFactoryMetadata;
export enum R3FactoryTarget {
Directive = 0,
Component = 1,
Injectable = 2,
Pipe = 3,
NgModule = 4,
}
/**
* Resolved type of a dependency.
*
* Occasionally, dependencies will have special significance which is known statically. In that
* case the `R3ResolvedDependencyType` informs the factory generator that a particular dependency
* should be generated specially (usually by calling a special injection function instead of the
* standard one).
*/
export enum R3ResolvedDependencyType {
/**
* A normal token dependency.
*/
Token = 0,
/**
* The dependency is for an attribute.
*
* The token expression is a string representing the attribute name.
*/
Attribute = 1,
/**
* Injecting the `ChangeDetectorRef` token. Needs special handling when injected into a pipe.
*/
ChangeDetectorRef = 2,
/**
* An invalid dependency (no token could be determined). An error should be thrown at runtime.
*/
Invalid = 3,
}
/**
* Metadata representing a single dependency to be injected into a constructor or function call.
*/
export interface R3DependencyMetadata {
/**
* An expression representing the token or value to be injected.
*/
token: o.Expression;
/**
* An enum indicating whether this dependency has special meaning to Angular and needs to be
* injected specially.
*/
resolved: R3ResolvedDependencyType;
/**
* Whether the dependency has an @Host qualifier.
*/
host: boolean;
/**
* Whether the dependency has an @Optional qualifier.
*/
optional: boolean;
/**
* Whether the dependency has an @Self qualifier.
*/
self: boolean;
/**
* Whether the dependency has an @SkipSelf qualifier.
*/
skipSelf: boolean;
}
export interface R3FactoryFn {
factory: o.Expression;
statements: o.Statement[];
type: o.ExpressionType;
}
/**
* Construct a factory function expression for the given `R3FactoryMetadata`.
*/
export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
const t = o.variable('t');
const statements: o.Statement[] = [];
// The type to instantiate via constructor invocation. If there is no delegated factory, meaning
// this type is always created by constructor invocation, then this is the type-to-create
// parameter provided by the user (t) if specified, or the current type if not. If there is a
// delegated factory (which is used to create the current type) then this is only the type-to-
// create parameter (t).
const typeForCtor = !isDelegatedMetadata(meta) ?
new o.BinaryOperatorExpr(o.BinaryOperator.Or, t, meta.internalType) :
t;
let ctorExpr: o.Expression|null = null;
if (meta.deps !== null) {
// There is a constructor (either explicitly or implicitly defined).
if (meta.deps !== 'invalid') {
ctorExpr = new o.InstantiateExpr(
typeForCtor,
injectDependencies(meta.deps, meta.injectFn, meta.target === R3FactoryTarget.Pipe));
}
} else {
const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
const getInheritedFactory = o.importExpr(R3.getInheritedFactory);
const baseFactoryStmt =
baseFactory.set(getInheritedFactory.callFn([meta.internalType]))
.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Exported, o.StmtModifier.Final]);
statements.push(baseFactoryStmt);
// There is no constructor, use the base class' factory to construct typeForCtor.
ctorExpr = baseFactory.callFn([typeForCtor]);
}
const ctorExprFinal = ctorExpr;
const body: o.Statement[] = [];
let retExpr: o.Expression|null = null;
function makeConditionalFactory(nonCtorExpr: o.Expression): o.ReadVarExpr {
const r = o.variable('r');
body.push(r.set(o.NULL_EXPR).toDeclStmt());
let ctorStmt: o.Statement|null = null;
if (ctorExprFinal !== null) {
ctorStmt = r.set(ctorExprFinal).toStmt();
} else {
ctorStmt = o.importExpr(R3.invalidFactory).callFn([]).toStmt();
}
body.push(o.ifStmt(t, [ctorStmt], [r.set(nonCtorExpr).toStmt()]));
return r;
}
if (isDelegatedMetadata(meta) && meta.delegateType === R3FactoryDelegateType.Factory) {
const delegateFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
const getFactoryOf = o.importExpr(R3.getFactoryOf);
if (meta.delegate.isEquivalent(meta.internalType)) {
throw new Error(`Illegal state: compiling factory that delegates to itself`);
}
const delegateFactoryStmt =
delegateFactory.set(getFactoryOf.callFn([meta.delegate])).toDeclStmt(o.INFERRED_TYPE, [
o.StmtModifier.Exported, o.StmtModifier.Final
]);
statements.push(delegateFactoryStmt);
retExpr = makeConditionalFactory(delegateFactory.callFn([]));
} else if (isDelegatedMetadata(meta)) {
// This type is created with a delegated factory. If a type parameter is not specified, call
// the factory instead.
const delegateArgs =
injectDependencies(meta.delegateDeps, meta.injectFn, meta.target === R3FactoryTarget.Pipe);
// Either call `new delegate(...)` or `delegate(...)` depending on meta.delegateType.
const factoryExpr = new (
meta.delegateType === R3FactoryDelegateType.Class ?
o.InstantiateExpr :
o.InvokeFunctionExpr)(meta.delegate, delegateArgs);
retExpr = makeConditionalFactory(factoryExpr);
} else if (isExpressionFactoryMetadata(meta)) {
// TODO(alxhub): decide whether to lower the value here or in the caller
retExpr = makeConditionalFactory(meta.expression);
} else {
retExpr = ctorExpr;
}
if (retExpr !== null) {
body.push(new o.ReturnStatement(retExpr));
} else {
body.push(o.importExpr(R3.invalidFactory).callFn([]).toStmt());
}
return {
factory: o.fn(
[new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined,
`${meta.name}_Factory`),
statements,
type: o.expressionType(
o.importExpr(R3.FactoryDef, [typeWithParameters(meta.type, meta.typeArgumentCount)]))
};
}
function injectDependencies(
deps: R3DependencyMetadata[], injectFn: o.ExternalReference, isPipe: boolean): o.Expression[] {
return deps.map((dep, index) => compileInjectDependency(dep, injectFn, isPipe, index));
}
function compileInjectDependency(
dep: R3DependencyMetadata, injectFn: o.ExternalReference, isPipe: boolean,
index: number): o.Expression {
// Interpret the dependency according to its resolved type.
switch (dep.resolved) {
case R3ResolvedDependencyType.Token:
case R3ResolvedDependencyType.ChangeDetectorRef:
// Build up the injection flags according to the metadata.
const flags = InjectFlags.Default | (dep.self ? InjectFlags.Self : 0) |
(dep.skipSelf ? InjectFlags.SkipSelf : 0) | (dep.host ? InjectFlags.Host : 0) |
(dep.optional ? InjectFlags.Optional : 0);
// If this dependency is optional or otherwise has non-default flags, then additional
// parameters describing how to inject the dependency must be passed to the inject function
// that's being used.
let flagsParam: o.LiteralExpr|null =
(flags !== InjectFlags.Default || dep.optional) ? o.literal(flags) : null;
// We have a separate instruction for injecting ChangeDetectorRef into a pipe.
if (isPipe && dep.resolved === R3ResolvedDependencyType.ChangeDetectorRef) {
return o.importExpr(R3.injectPipeChangeDetectorRef).callFn(flagsParam ? [flagsParam] : []);
}
// Build up the arguments to the injectFn call.
const injectArgs = [dep.token];
if (flagsParam) {
injectArgs.push(flagsParam);
}
return o.importExpr(injectFn).callFn(injectArgs);
case R3ResolvedDependencyType.Attribute:
// In the case of attributes, the attribute name in question is given as the token.
return o.importExpr(R3.injectAttribute).callFn([dep.token]);
case R3ResolvedDependencyType.Invalid:
return o.importExpr(R3.invalidFactoryDep).callFn([o.literal(index)]);
default:
return unsupported(
`Unknown R3ResolvedDependencyType: ${R3ResolvedDependencyType[dep.resolved]}`);
}
}
/**
* A helper function useful for extracting `R3DependencyMetadata` from a Render2
* `CompileTypeMetadata` instance.
*/
export function dependenciesFromGlobalMetadata(
type: CompileTypeMetadata, outputCtx: OutputContext,
reflector: CompileReflector): R3DependencyMetadata[] {
// Use the `CompileReflector` to look up references to some well-known Angular types. These will
// be compared with the token to statically determine whether the token has significance to
// Angular, and set the correct `R3ResolvedDependencyType` as a result.
const injectorRef = reflector.resolveExternalReference(Identifiers.Injector);
// Iterate through the type's DI dependencies and produce `R3DependencyMetadata` for each of them.
const deps: R3DependencyMetadata[] = [];
for (let dependency of type.diDeps) {
if (dependency.token) {
const tokenRef = tokenReference(dependency.token);
let resolved: R3ResolvedDependencyType = dependency.isAttribute ?
R3ResolvedDependencyType.Attribute :
R3ResolvedDependencyType.Token;
// In the case of most dependencies, the token will be a reference to a type. Sometimes,
// however, it can be a string, in the case of older Angular code or @Attribute injection.
const token =
tokenRef instanceof StaticSymbol ? outputCtx.importExpr(tokenRef) : o.literal(tokenRef);
// Construct the dependency.
deps.push({
token,
resolved,
host: !!dependency.isHost,
optional: !!dependency.isOptional,
self: !!dependency.isSelf,
skipSelf: !!dependency.isSkipSelf,
});
} else {
unsupported('dependency without a token');
}
}
return deps;
}
function isDelegatedMetadata(meta: R3FactoryMetadata): meta is R3DelegatedFactoryMetadata|
R3DelegatedFnOrClassMetadata {
return (meta as any).delegateType !== undefined;
}
function isExpressionFactoryMetadata(meta: R3FactoryMetadata): meta is R3ExpressionFactoryMetadata {
return (meta as any).expression !== undefined;
}