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:
Alex Rickabaugh
2019-01-31 14:23:54 -08:00
committed by Misko Hevery
parent f8b67712bc
commit d2742cf473
12 changed files with 264 additions and 40 deletions

View File

@ -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,