refactor(ivy): generate ngFactoryDef for injectables (#32433)

With #31953 we moved the factories for components, directives and pipes into a new field called `ngFactoryDef`, however I decided not to do it for injectables, because they needed some extra logic. These changes set up the `ngFactoryDef` for injectables as well.

For reference, the extra logic mentioned above is that for injectables we have two code paths:

1. For injectables that don't configure how they should be instantiated, we create a `factory` that proxies to `ngFactoryDef`:

```
// Source
@Injectable()
class Service {}

// Output
class Service {
  static ngInjectableDef = defineInjectable({
    factory: () => Service.ngFactoryFn(),
  });

  static ngFactoryFn: (t) => new (t || Service)();
}
```

2. For injectables that do configure how they're created, we keep the `ngFactoryDef` and generate the factory based on the metadata:

```
// Source
@Injectable({
  useValue: DEFAULT_IMPL,
})
class Service {}

// Output
export class Service {
  static ngInjectableDef = defineInjectable({
    factory: () => DEFAULT_IMPL,
  });

  static ngFactoryFn: (t) => new (t || Service)();
}
```

PR Close #32433
This commit is contained in:
crisbeto
2019-09-01 12:26:04 +02:00
committed by atscott
parent 2729747225
commit 4e35e348af
33 changed files with 695 additions and 295 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
@ -486,7 +486,7 @@ export class ComponentDecoratorHandler implements
CompileResult[] {
const meta = analysis.meta;
const res = compileComponentFromMetadata(meta, pool, makeBindingParser());
const factoryRes = compileNgFactoryDefField(meta);
const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject});
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler';
import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, Identifiers, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -94,7 +94,7 @@ export class DirectiveDecoratorHandler implements
CompileResult[] {
const meta = analysis.meta;
const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser());
const factoryRes = compileNgFactoryDefField(meta);
const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject});
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
import {Expression, Identifiers, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -14,12 +14,15 @@ import {DefaultImportRecorder} from '../../imports';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, validateConstructorDependencies} from './util';
import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, unwrapForwardRef, validateConstructorDependencies} from './util';
export interface InjectableHandlerData {
meta: R3InjectableMetadata;
metadataStmt: Statement|null;
ctorDeps: R3DependencyMetadata[]|'invalid'|null;
needsFactory: boolean;
}
/**
@ -49,41 +52,65 @@ export class InjectableDecoratorHandler implements
}
analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput<InjectableHandlerData> {
const meta = extractInjectableMetadata(node, decorator, this.reflector);
const decorators = this.reflector.getDecoratorsOfDeclaration(node);
return {
analysis: {
meta: extractInjectableMetadata(
node, decorator, this.reflector, this.defaultImportRecorder, this.isCore,
meta,
ctorDeps: extractInjectableCtorDeps(
node, meta, decorator, this.reflector, this.defaultImportRecorder, this.isCore,
this.strictCtorDeps),
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore),
// Avoid generating multiple factories if a class has
// more Angular decorators, apart from Injectable.
needsFactory: !decorators ||
decorators.every(current => !isAngularCore(current) || current.name === 'Injectable')
},
};
}
compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult {
compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult[] {
const res = compileIvyInjectable(analysis.meta);
const statements = res.statements;
if (analysis.metadataStmt !== null) {
statements.push(analysis.metadataStmt);
const results: CompileResult[] = [];
if (analysis.needsFactory) {
const meta = analysis.meta;
const factoryRes = compileNgFactoryDefField({
name: meta.name,
type: meta.type,
typeArgumentCount: meta.typeArgumentCount,
deps: analysis.ctorDeps,
injectFn: Identifiers.inject
});
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}
results.push(factoryRes);
}
return {
results.push({
name: 'ngInjectableDef',
initializer: res.expression, statements,
type: res.type,
};
});
return results;
}
}
/**
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the input
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the
* input
* metadata needed to run `compileIvyInjectable`.
*
* A `null` return value indicates this is @Injectable has invalid data.
*/
function extractInjectableMetadata(
clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost,
defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
strictCtorDeps: boolean): R3InjectableMetadata {
clazz: ClassDeclaration, decorator: Decorator,
reflector: ReflectionHost): R3InjectableMetadata {
const name = clazz.name.text;
const type = new WrappedNodeExpr(clazz.name);
const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0;
@ -92,53 +119,13 @@ function extractInjectableMetadata(
ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
}
if (decorator.args.length === 0) {
// Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be
// subject to the same validation. However, existing Angular code abuses @Injectable, applying
// it to things like abstract classes with constructors that were never meant for use with
// Angular's DI.
//
// To deal with this, @Injectable() without an argument is more lenient, and if the constructor
// signature does not work for DI then an ngInjectableDef that throws.
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
if (strictCtorDeps) {
ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
} else {
const possibleCtorDeps =
getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
if (possibleCtorDeps !== null) {
if (possibleCtorDeps.deps !== null) {
// This use of @Injectable has valid constructor dependencies.
ctorDeps = possibleCtorDeps.deps;
} else {
// This use of @Injectable is technically invalid. Generate a factory function which
// throws
// an error.
// TODO(alxhub): log warnings for the bad use of @Injectable.
ctorDeps = 'invalid';
}
}
}
return {
name,
type,
typeArgumentCount,
providedIn: new LiteralExpr(null), ctorDeps,
providedIn: new LiteralExpr(null),
};
} else if (decorator.args.length === 1) {
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
// rawCtorDeps will be null if the class has no constructor.
if (rawCtorDeps !== null) {
if (rawCtorDeps.deps !== null) {
// A constructor existed and had valid dependencies.
ctorDeps = rawCtorDeps.deps;
} else {
// A constructor existed but had invalid dependencies.
ctorDeps = 'invalid';
}
}
const metaNode = decorator.args[0];
// Firstly make sure the decorator argument is an inline literal - if not, it's illegal to
// transport references from one location to another. This is the problem that lowering
@ -170,27 +157,25 @@ function extractInjectableMetadata(
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useValue: new WrappedNodeExpr(meta.get('useValue') !),
useValue: new WrappedNodeExpr(unwrapForwardRef(meta.get('useValue') !, reflector)),
};
} else if (meta.has('useExisting')) {
return {
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useExisting: new WrappedNodeExpr(meta.get('useExisting') !),
useExisting: new WrappedNodeExpr(unwrapForwardRef(meta.get('useExisting') !, reflector)),
};
} else if (meta.has('useClass')) {
return {
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useClass: new WrappedNodeExpr(meta.get('useClass') !), userDeps,
useClass: new WrappedNodeExpr(unwrapForwardRef(meta.get('useClass') !, reflector)),
userDeps,
};
} else if (meta.has('useFactory')) {
// useFactory is special - the 'deps' property must be analyzed.
@ -200,14 +185,10 @@ function extractInjectableMetadata(
type,
typeArgumentCount,
providedIn,
useFactory: factory, ctorDeps, userDeps,
useFactory: factory, userDeps,
};
} else {
if (strictCtorDeps) {
// Since use* was not provided, validate the deps according to strictCtorDeps.
validateConstructorDependencies(clazz, rawCtorDeps);
}
return {name, type, typeArgumentCount, providedIn, ctorDeps};
return {name, type, typeArgumentCount, providedIn};
}
} else {
throw new FatalDiagnosticError(
@ -215,7 +196,69 @@ function extractInjectableMetadata(
}
}
function extractInjectableCtorDeps(
clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator,
reflector: ReflectionHost, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
strictCtorDeps: boolean) {
if (decorator.args === null) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
}
let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null;
if (decorator.args.length === 0) {
// Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be
// subject to the same validation. However, existing Angular code abuses @Injectable, applying
// it to things like abstract classes with constructors that were never meant for use with
// Angular's DI.
//
// To deal with this, @Injectable() without an argument is more lenient, and if the
// constructor
// signature does not work for DI then an ngInjectableDef that throws.
if (strictCtorDeps) {
ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
} else {
const possibleCtorDeps =
getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
if (possibleCtorDeps !== null) {
if (possibleCtorDeps.deps !== null) {
// This use of @Injectable has valid constructor dependencies.
ctorDeps = possibleCtorDeps.deps;
} else {
// This use of @Injectable is technically invalid. Generate a factory function which
// throws
// an error.
// TODO(alxhub): log warnings for the bad use of @Injectable.
ctorDeps = 'invalid';
}
}
}
return ctorDeps;
} else if (decorator.args.length === 1) {
const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore);
// rawCtorDeps will be null if the class has no constructor.
if (rawCtorDeps !== null) {
if (rawCtorDeps.deps !== null) {
// A constructor existed and had valid dependencies.
ctorDeps = rawCtorDeps.deps;
} else {
// A constructor existed but had invalid dependencies.
ctorDeps = 'invalid';
}
}
if (strictCtorDeps && !meta.useValue && !meta.useExisting && !meta.useClass &&
!meta.useFactory) {
// Since use* was not provided, validate the deps according to strictCtorDeps.
validateConstructorDependencies(clazz, rawCtorDeps);
}
}
return ctorDeps;
}
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
const meta: R3DependencyMetadata = {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
import {Identifiers, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -109,7 +109,11 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
compile(node: ClassDeclaration, analysis: PipeHandlerData): CompileResult[] {
const meta = analysis.meta;
const res = compilePipeFromMetadata(meta);
const factoryRes = compileNgFactoryDefField({...meta, isPipe: true});
const factoryRes = compileNgFactoryDefField({
...meta,
injectFn: Identifiers.directiveInject,
isPipe: true,
});
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}

View File

@ -53,6 +53,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
['ɵɵdefineNgModule', 'ɵɵdefineNgModule'],
['ɵɵsetNgModuleScope', 'ɵɵsetNgModuleScope'],
['ɵɵinject', 'ɵɵinject'],
['ɵɵFactoryDef', 'ɵɵFactoryDef'],
['ɵsetClassMetadata', 'setClassMetadata'],
['ɵɵInjectableDef', 'ɵɵInjectableDef'],
['ɵɵInjectorDef', 'ɵɵInjectorDef'],

View File

@ -353,10 +353,14 @@ export class IvyCompilation {
const compileMatchRes =
match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool);
this.perf.stop(compileSpan);
if (!Array.isArray(compileMatchRes)) {
if (Array.isArray(compileMatchRes)) {
compileMatchRes.forEach(result => {
if (!res.some(r => r.name === result.name)) {
res.push(result);
}
});
} else if (!res.some(result => result.name === compileMatchRes.name)) {
res.push(compileMatchRes);
} else {
res.push(...compileMatchRes);
}
}