Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ivy): support generation of flags for directive injection #23345

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/compiler-cli/test/ngc_spec.ts
Expand Up @@ -2237,7 +2237,7 @@ describe('ngc transformer command-line', () => {
constructor(e: Existing|null) {}
}
`);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, null, 0\)/);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, 8\)/);
});

it('compiles a useFactory InjectableDef with skip-self dep', () => {
Expand All @@ -2257,7 +2257,7 @@ describe('ngc transformer command-line', () => {
constructor(e: Existing) {}
}
`);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, undefined, 1\)/);
expect(source).toMatch(/ngInjectableDef.*return ..\(..\.inject\(Existing, 4\)/);
});

it('compiles a service that depends on a token', () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/compiler/src/core.ts
Expand Up @@ -217,14 +217,23 @@ export const enum DepFlags {
Value = 1 << 3,
}

/** Injection flags for DI. */
/**
* Injection flags for DI.
*/
export const enum InjectFlags {
Default = 0,

/** Skip the node that is requesting injection. */
SkipSelf = 1 << 0,
/**
* Specifies that an injector should retrieve a dependency from any injector until reaching the
* host element of the current component. (Only used with Element Injector)
*/
Host = 1 << 0,
/** Don't descend into ancestors of the node requesting injection. */
Self = 1 << 1,
/** Skip the node that is requesting injection. */
SkipSelf = 1 << 2,
/** Inject `defaultValue` instead if token not found. */
Optional = 1 << 3,
}

export const enum ArgumentType {Inline = 0, Dynamic = 1}
Expand Down
7 changes: 3 additions & 4 deletions packages/compiler/src/injectable_compiler.ts
Expand Up @@ -38,15 +38,14 @@ export class InjectableCompiler {
private depsArray(deps: any[], ctx: OutputContext): o.Expression[] {
return deps.map(dep => {
let token = dep;
let defaultValue = undefined;
let args = [token];
let flags: InjectFlags = InjectFlags.Default;
if (Array.isArray(dep)) {
for (let i = 0; i < dep.length; i++) {
const v = dep[i];
if (v) {
if (v.ngMetadataName === 'Optional') {
defaultValue = null;
flags |= InjectFlags.Optional;
} else if (v.ngMetadataName === 'SkipSelf') {
flags |= InjectFlags.SkipSelf;
} else if (v.ngMetadataName === 'Self') {
Expand All @@ -69,8 +68,8 @@ export class InjectableCompiler {
tokenExpr = ctx.importExpr(token);
}

if (flags !== InjectFlags.Default || defaultValue !== undefined) {
args = [tokenExpr, o.literal(defaultValue), o.literal(flags)];
if (flags !== InjectFlags.Default) {
args = [tokenExpr, o.literal(flags)];
} else {
args = [tokenExpr];
}
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/render3/r3_identifiers.ts
Expand Up @@ -81,14 +81,16 @@ export class Identifiers {

static directiveLifeCycle: o.ExternalReference = {name: 'ɵl', moduleName: CORE};

static injectAttribute: o.ExternalReference = {name: 'ɵinjectAttribute', moduleName: CORE};

static injectElementRef: o.ExternalReference = {name: 'ɵinjectElementRef', moduleName: CORE};

static injectTemplateRef: o.ExternalReference = {name: 'ɵinjectTemplateRef', moduleName: CORE};

static injectViewContainerRef:
o.ExternalReference = {name: 'ɵinjectViewContainerRef', moduleName: CORE};

static inject: o.ExternalReference = {name: 'ɵinject', moduleName: CORE};
static directiveInject: o.ExternalReference = {name: 'ɵdirectiveInject', moduleName: CORE};

static defineComponent: o.ExternalReference = {name: 'ɵdefineComponent', moduleName: CORE};

Expand Down
43 changes: 34 additions & 9 deletions packages/compiler/src/render3/r3_view_compiler.ts
Expand Up @@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/

import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, CompileTypeSummary, flatten, identifierName, rendererTypeName, sanitizeIdentifier, tokenReference, viewClassName} from '../compile_metadata';
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, CompileTypeSummary, flatten, identifierName, rendererTypeName, sanitizeIdentifier, tokenReference, viewClassName} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector';
import {BindingForm, BuiltinConverter, BuiltinFunctionCall, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../constant_pool';
import {InjectFlags} from '../core';
import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast';
import {Identifiers} from '../identifiers';
import {LifecycleHooks} from '../lifecycle_reflector';
Expand All @@ -19,9 +20,11 @@ import {CssSelector} from '../selector';
import {BindingParser} from '../template_parser/binding_parser';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {OutputContext, error} from '../util';

import {Identifiers as R3} from './r3_identifiers';
import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types';


/** Name of the context parameter passed into a template function */
const CONTEXT_NAME = 'ctx';

Expand Down Expand Up @@ -927,12 +930,6 @@ export function createFactory(
const viewContainerRef = reflector.resolveExternalReference(Identifiers.ViewContainerRef);

for (let dependency of type.diDeps) {
if (dependency.isValue) {
unsupported('value dependencies');
}
if (dependency.isHost) {
unsupported('host dependencies');
}
const token = dependency.token;
if (token) {
const tokenRef = tokenReference(token);
Expand All @@ -942,10 +939,18 @@ export function createFactory(
args.push(o.importExpr(R3.injectTemplateRef).callFn([]));
} else if (tokenRef === viewContainerRef) {
args.push(o.importExpr(R3.injectViewContainerRef).callFn([]));
} else if (dependency.isAttribute) {
args.push(o.importExpr(R3.injectAttribute).callFn([o.literal(dependency.token !.value)]));
} else {
const value =
const tokenValue =
token.identifier != null ? outputCtx.importExpr(tokenRef) : o.literal(tokenRef);
args.push(o.importExpr(R3.inject).callFn([value]));
const directiveInjectArgs = [tokenValue];
const flags = extractFlags(dependency);
if (flags != InjectFlags.Default) {
// Append flag information if other than default.
directiveInjectArgs.push(o.literal(flags));
}
args.push(o.importExpr(R3.directiveInject).callFn(directiveInjectArgs));
}
} else {
unsupported('dependency without a token');
Expand Down Expand Up @@ -979,6 +984,26 @@ export function createFactory(
type.reference.name ? `${type.reference.name}_Factory` : null);
}

function extractFlags(dependency: CompileDiDependencyMetadata): InjectFlags {
let flags = InjectFlags.Default;
if (dependency.isHost) {
flags |= InjectFlags.Host;
}
if (dependency.isOptional) {
flags |= InjectFlags.Optional;
}
if (dependency.isSelf) {
flags |= InjectFlags.Self;
}
if (dependency.isSkipSelf) {
flags |= InjectFlags.SkipSelf;
}
if (dependency.isValue) {
unsupported('value dependencies');
}
return flags;
}

/**
* Remove trailing null nodes as they are implied.
*/
Expand Down
70 changes: 70 additions & 0 deletions packages/compiler/test/render3/r3_view_compiler_di_spec.ts
@@ -0,0 +1,70 @@
/**
* @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 {MockDirectory, setup} from '../aot/test_util';
import {compile, expectEmit} from './mock_compile';

describe('compiler compliance: dependency injection', () => {
const angularFiles = setup({
compileAngular: true,
compileAnimations: false,
compileCommon: true,
});

it('should create factory methods', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Injectable, Attribute, Host, SkipSelf, Self, Optional} from '@angular/core';
import {CommonModule} from '@angular/common';

@Injectable()
export class MyService {}

@Component({
selector: 'my-component',
template: \`\`
})
export class MyComponent {
constructor(
@Attribute('name') name:string,
s1: MyService,
@Host() s2: MyService,
@Self() s4: MyService,
@SkipSelf() s3: MyService,
@Optional() s5: MyService,
@Self() @Optional() s6: MyService,
) {}
}

@NgModule({declarations: [MyComponent], imports: [CommonModule], providers: [MyService]})
export class MyModule {}
`
}
};

const factory = `
factory: function MyComponent_Factory() {
return new MyComponent(
$r3$.ɵinjectAttribute('name'),
$r3$.ɵdirectiveInject(MyService),
$r3$.ɵdirectiveInject(MyService, 1),
$r3$.ɵdirectiveInject(MyService, 2),
$r3$.ɵdirectiveInject(MyService, 4),
$r3$.ɵdirectiveInject(MyService, 8),
$r3$.ɵdirectiveInject(MyService, 10)
);
}`;


const result = compile(files, angularFiles);

expectEmit(result.source, factory, 'Incorrect factory');
});

});
Expand Up @@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectableDef, defineInjectable} from '../../di/defs';
import {Optional, SkipSelf} from '../../di/metadata';
import {StaticProvider} from '../../di/provider';
import {DefaultIterableDifferFactory} from '../differs/default_iterable_differ';


/**
Expand Down Expand Up @@ -135,6 +137,11 @@ export interface IterableDifferFactory {
*
*/
export class IterableDiffers {
static ngInjectableDef: InjectableDef<IterableDiffers> = defineInjectable({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would compilation result lives in source code? Shouldn't it using @Injectable() decorator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few reasons:
1.) we currently don't compile core with ivy so the decorator does not get lowered.
2.) There is a circular dependency between the compiler and core which makes it hard for the compiler to compile tho core. Rather than solving that, we have opted for hand coding it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should probably be in the source code

providedIn: 'root',
factory: () => new IterableDiffers([new DefaultIterableDifferFactory()])
});

/**
* @deprecated v4.0.0 - Should be private
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Expand Up @@ -13,7 +13,7 @@ export {devModeEqual as ɵdevModeEqual} from './change_detection/change_detectio
export {isListLikeIterable as ɵisListLikeIterable} from './change_detection/change_detection_util';
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';
export {Console as ɵConsole} from './console';
export {setCurrentInjector as ɵsetCurrentInjector} from './di/injector';
export {inject as ɵinject, setCurrentInjector as ɵsetCurrentInjector} from './di/injector';
export {APP_ROOT as ɵAPP_ROOT} from './di/scope';
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver';
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/core_render3_private_export.ts
Expand Up @@ -21,7 +21,6 @@ export {
injectViewContainerRef as ɵinjectViewContainerRef,
injectChangeDetectorRef as ɵinjectChangeDetectorRef,
injectAttribute as ɵinjectAttribute,
InjectFlags as ɵInjectFlags,
PublicFeature as ɵPublicFeature,
NgOnChangesFeature as ɵNgOnChangesFeature,
CssSelectorList as ɵCssSelectorList,
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/di/defs.ts
Expand Up @@ -25,8 +25,25 @@ import {ClassProvider, ClassSansProvider, ConstructorProvider, ConstructorSansPr
* @experimental
*/
export interface InjectableDef<T> {
/**
* Specifies that the given type belongs to a particular injector:
* - `InjectorType` such as `NgModule`,
* - `'root'` the root injector
* - `'any'` all injectors.
* - `null`, does not belong to any injector. Must be explicitly listed in the injector
* `providers`.
*/
providedIn: InjectorType<any>|'root'|'any'|null;

/**
* Factory method to execute to create an instance of the injectable.
*/
factory: () => T;

/**
* In a case of no explicit injector, a location where the instance of the injectable is stored.
*/
value: T|undefined;
}

/**
Expand Down Expand Up @@ -101,12 +118,13 @@ export interface InjectorTypeWithProviders<T> {
* @experimental
*/
export function defineInjectable<T>(opts: {
providedIn?: Type<any>| 'root' | null,
providedIn?: Type<any>| 'root' | 'any' | null,
factory: () => T,
}): InjectableDef<T> {
return {
providedIn: (opts.providedIn as InjectorType<any>| 'root' | null | undefined) || null,
providedIn: opts.providedIn as any || null,
factory: opts.factory,
value: undefined,
};
}

Expand Down