feat(ivy): compile @Injectable on classes not meant for DI (#28523)
In the past, @Injectable had no side effects and existing Angular code is therefore littered with @Injectable usage on classes which are not intended to be injected. A common example is: @Injectable() class Foo { constructor(private notInjectable: string) {} } and somewhere else: providers: [{provide: Foo, useFactory: ...}) Here, there is no need for Foo to be injectable - indeed, it's impossible for the DI system to create an instance of it, as it has a non-injectable constructor. The provider configures a factory for the DI system to be able to create instances of Foo. Adding @Injectable in Ivy signifies that the class's own constructor, and not a provider, determines how the class will be created. This commit adds logic to compile classes which are marked with @Injectable but are otherwise not injectable, and create an ngInjectableDef field with a factory function that throws an error. This way, existing code in the wild continues to compile, but if someone attempts to use the injectable it will fail with a useful error message. In the case where strictInjectionParameters is set to true, a compile-time error is thrown instead of the runtime error, as ngtsc has enough information to determine when injection couldn't possibly be valid. PR Close #28523
This commit is contained in:

committed by
Misko Hevery

parent
f8b67712bc
commit
d2742cf473
@ -22,7 +22,7 @@ export interface R3InjectableMetadata {
|
||||
name: string;
|
||||
type: o.Expression;
|
||||
typeArgumentCount: number;
|
||||
ctorDeps: R3DependencyMetadata[]|null;
|
||||
ctorDeps: R3DependencyMetadata[]|'invalid'|null;
|
||||
providedIn: o.Expression;
|
||||
useClass?: o.Expression;
|
||||
useFactory?: o.Expression;
|
||||
@ -46,11 +46,14 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
|
||||
// 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, in which
|
||||
// case omitting deps just uses the constructor dependencies instead.
|
||||
// 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.isEquivalent(meta.type);
|
||||
const deps = meta.userDeps || (useClassOnSelf && meta.ctorDeps) || undefined;
|
||||
let deps: R3DependencyMetadata[]|undefined = undefined;
|
||||
if (meta.userDeps !== undefined) {
|
||||
deps = meta.userDeps;
|
||||
}
|
||||
|
||||
if (deps !== undefined) {
|
||||
// factory: () => new meta.useClass(...deps)
|
||||
@ -60,6 +63,8 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
|
||||
delegateDeps: deps,
|
||||
delegateType: R3FactoryDelegateType.Class,
|
||||
});
|
||||
} else if (useClassOnSelf) {
|
||||
result = compileFactoryFunction(factoryMeta);
|
||||
} else {
|
||||
result = compileFactoryFunction({
|
||||
...factoryMeta,
|
||||
|
@ -40,9 +40,11 @@ export interface R3ConstructorFactoryMetadata {
|
||||
* 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.
|
||||
* 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[]|null;
|
||||
deps: R3DependencyMetadata[]|'invalid'|null;
|
||||
|
||||
/**
|
||||
* An expression for the function which will be used to inject dependencies. The API of this
|
||||
@ -152,7 +154,9 @@ export function compileFactoryFunction(meta: R3FactoryMetadata):
|
||||
let ctorExpr: o.Expression|null = null;
|
||||
if (meta.deps !== null) {
|
||||
// There is a constructor (either explicitly or implicitly defined).
|
||||
ctorExpr = new o.InstantiateExpr(typeForCtor, injectDependencies(meta.deps, meta.injectFn));
|
||||
if (meta.deps !== 'invalid') {
|
||||
ctorExpr = new o.InstantiateExpr(typeForCtor, injectDependencies(meta.deps, meta.injectFn));
|
||||
}
|
||||
} else {
|
||||
const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
|
||||
const getInheritedFactory = o.importExpr(R3.getInheritedFactory);
|
||||
@ -173,7 +177,13 @@ export function compileFactoryFunction(meta: R3FactoryMetadata):
|
||||
function makeConditionalFactory(nonCtorExpr: o.Expression): o.ReadVarExpr {
|
||||
const r = o.variable('r');
|
||||
body.push(r.set(o.NULL_EXPR).toDeclStmt());
|
||||
body.push(o.ifStmt(t, [r.set(ctorExprFinal).toStmt()], [r.set(nonCtorExpr).toStmt()]));
|
||||
let ctorStmt: o.Statement|null = null;
|
||||
if (ctorExprFinal !== null) {
|
||||
ctorStmt = r.set(ctorExprFinal).toStmt();
|
||||
} else {
|
||||
ctorStmt = makeErrorStmt(meta.name);
|
||||
}
|
||||
body.push(o.ifStmt(t, [ctorStmt], [r.set(nonCtorExpr).toStmt()]));
|
||||
return r;
|
||||
}
|
||||
|
||||
@ -207,10 +217,16 @@ export function compileFactoryFunction(meta: R3FactoryMetadata):
|
||||
retExpr = ctorExpr;
|
||||
}
|
||||
|
||||
if (retExpr !== null) {
|
||||
body.push(new o.ReturnStatement(retExpr));
|
||||
} else {
|
||||
body.push(makeErrorStmt(meta.name));
|
||||
}
|
||||
|
||||
return {
|
||||
factory: o.fn(
|
||||
[new o.FnParam('t', o.DYNAMIC_TYPE)], [...body, new o.ReturnStatement(retExpr)],
|
||||
o.INFERRED_TYPE, undefined, `${meta.name}_Factory`),
|
||||
[new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined,
|
||||
`${meta.name}_Factory`),
|
||||
statements,
|
||||
};
|
||||
}
|
||||
@ -292,6 +308,13 @@ export function dependenciesFromGlobalMetadata(
|
||||
return deps;
|
||||
}
|
||||
|
||||
function makeErrorStmt(name: string): o.Statement {
|
||||
return new o.ThrowStmt(new o.InstantiateExpr(new o.ReadVarExpr('Error'), [
|
||||
o.literal(
|
||||
`${name} has a constructor which is not compatible with Dependency Injection. It should probably not be @Injectable().`)
|
||||
]));
|
||||
}
|
||||
|
||||
function isDelegatedMetadata(meta: R3FactoryMetadata): meta is R3DelegatedFactoryMetadata|
|
||||
R3DelegatedFnOrClassMetadata {
|
||||
return (meta as any).delegateType !== undefined;
|
||||
|
Reference in New Issue
Block a user