11. Extended Fetaures

11.1. Array and Object Destructuring

N4JS supports array and object destructuring as provided in ES6. This is used to conveniently assign selected elements of an array or object to a number of newly-declared or pre-existing variables or to further destructure them by using nested destructuring patterns [55].

11.1.1. Syntax

BindingPattern <Yield>:
    ObjectBindingPattern<Yield>
    | ArrayBindingPattern<Yield>
;

ObjectBindingPattern <Yield> returns BindingPattern:
    {BindingPattern}
    '{' (properties+=BindingProperty<Yield,AllowType=false> (',' properties+=BindingProperty<Yield,AllowType=false>)*)? '}'
;

ArrayBindingPattern <Yield> returns BindingPattern:
    {BindingPattern}
    '['
        elements+=Elision* (
            elements+=BindingRestElement<Yield>
            (',' elements+=Elision* elements+=BindingRestElement<Yield>)*
            (',' elements+=Elision*)?
        )?
    ']'
;

BindingProperty <Yield, AllowType>:
      =>(LiteralBindingPropertyName<Yield> ':') value=BindingElement<Yield>
    | value=SingleNameBinding<Yield,AllowType>
;

fragment LiteralBindingPropertyName <Yield>*:
    declaredName=IdentifierName | declaredName=STRING | declaredName=NumericLiteralAsString
    // this is added here due to special treatment for a known set of expressions
    | '[' (declaredName=SymbolLiteralComputedName<Yield> | declaredName=STRING) ']'
;

11.1.2. Semantics

The following example declares four variables a, b, x, and prop2. Variables a and x will have the value hello, whereas b and prop2 will have value 42.

var [a,b] = ["hello", 42];

var {prop1:x, prop2} = {prop1:"hello", prop2:42};

In the case of prop2, we do not provide a property name and variable name separately; this is useful in cases where the property name also makes for a suitable variable name (called single name binding).

One of the most useful use cases of destructuring is in a for..of loop. Take this example:

var arr1 = [ ["hello",1,2,3], ["goodbye",4,5,6] ];
for(var [head,...tail] of arr1) {
    console.log(head,'/',tail);
}
// will print:
//   hello / [ 1, 2, 3 ]
//   goodbye / [ 4, 5, 6 ]

var arr2 = [ {key:"hello", value:42}, {key:"goodbye", value:43} ];
for(var {key,value} of arr2) {
    console.log(key,'/',value);
}
// will print:
//   hello / 42
//   goodbye / 43

Array and object destructuring pattern can appear in many different places:

  • In a variable declaration (not just in variable statements but also in other places where variable declarations are allowed, e.g. plain for loops; called destructuring binding; see Variable Statement).

  • On the left-hand side of an assignment expression (the assignment expression is then called destructuring assignment; see Assignment Expression).

  • In a for..in or for..of loop on the left side of the in/of (see for …​ of statement).

    It can also be used in plain statements, but then we actually have one of the above two use cases.
  • With lists of formal parameters or function arguments (not supported yet).

For further details on array and object destructuring please refer to the ECMAScript 6 specification - [ECMA15a].

Type annotations can only be added when a new variable name is introduced since the short version would be ambiguous with the long one. For example:

var {x: someTypeOrNewVar} = ol

could either mean that a new variable someTypeOrNewVar is declared and ol.x is assigned to it, or that a new variable x is declared with type someTypeOrNewVar. The longer form would look like this:

var {x: x: someType} = ol

We can make this more readable:

var {propOfOl: newVar: typeOfNewVar} = ol

11.2. Dependency Injection

This chapter describes DI mechanisms for N4JS. This includes compiler, validation and language extensions that allow to achieve DI mechanisms built in into the N4JS language and IDE.

N4JS DI support specifies a means for obtaining objects in such a way as to maximize reusability, testability and maintainability, especially compared to traditional approaches such as constructors, factories and service locators. While this can be achieved manually (without tooling support) it is difficult for nontrivial applications. The solutions that DI provides should empower N4JS users to achieve the above goals without the burden of maintaining so-called ’boilerplate’ code.

diBasicTerms
Figure 8. DI Basic Terms

key: pass the dependency instead of letting the client create or find it

Core terms

  • Service - A set of APIs describing the functionality of the service.

  • Service Implementations - One or more implementations of given service API.

  • Client - Consumer of a given functionality, uses the given Service Implementation.

  • Injector - Object providing Service Implementation of a specific Service, according to configuration.

  • Binding - Part of configuration describing which interface implementing a subtype will be injected, when a given interface is requested.

  • Provider - Factory used to create instances of a given Service Implementation or its sub-components, can be a method.

  • Injection Point - Part of the user’s code that will have the given dependency injected. This is usually fields, method parameters, constructor parameters etc.

  • DI configuration - This describes which elements of the user’s code are used in mechanisms and how they are wired. It is derived from user code elements being marked with appropriate annotations, bindings and providers.

  • di wiring - The code responsible for creating user objects. These are injectors, type factories/providers, fields initiators etc.

11.2.1. DI Components and Injectors

N4JS’ Dependency Injection systems is based on the notion of DIC.

Definition: DI Component

A DIC is a N4Class annotated with @GenerateInjector.

This annotation causes an injector to be created for (and associated to) the DI. DIC can be composed; meaning that when requested to inject an instance of a type, a DIC’s injector can delegate this request to the injector of the containing DIC. An injector always prioritizes its own configuration before delegating to the container’s injector. For validation purposes, a child DI can be annotated with @WithParent to ensure that it is always used with a proper parent.

Injector is the main object of DI mechanisms responsible for creating object graphs of the application. At runtime, injectors are instances of N4Injector.

Req. IDE-138: DI Component and Injector (ver. 1)

The following constraints must hold for a class C marked as DIC:

  1. A subclass S of C is a DIC as well and it must be marked with GenerateInjector.

  2. If a parent DIC P is specified via WithParent, then P must be a DIC as well.

  3. The injector associated to a DIC is of type N4Injector. It can be retrieved via N4Injector.of(DIC) in which DIC is the DIC.

  4. Injectors associated to DIC a are DI-singletons (cf. Singleton Scope). Two calls to N4Injector.of(DIC) are different (as different DIC are assumed).

Req. IDE-139: Injection Phase (ver. 1)

We call the (transitive) creation and setting of values by an injector I caused by the creation of an root object R the injection phase. If an instance C is newly created by the injector I (regardless of the injection point being used), the injection is transitively applied on C. The following constraints have to hold:

  1. Root objects are created by one of the following mechanisms:

    1. Any class or interface can be created as root objects via an injector associated to a DIC:
      var x: X = N4Injector.of(DIC).create(X);
      in which DIC is a DIC.

      Of course, an appropriate binding must exist. [56]

    2. If a type has the injector being injected, e.g. via field injection @Inject injector: N4Injector;, then this injector can be used anytime in the control flow to create a new root object similar as above (using create method).

    3. If a provider has been injected (i.e. an instance of {N4Provider}), then its get() method can be used to create a root object causing a new injection phase to take place.

  2. If C.ctor is marked as injection point, all its arguments are set by the injector. This is also true for an inherited constructor marked as an injection point. See [Req-IDE-143] . For all arguments the injection phase constraints have to hold as well.

  3. All fields of C, including inherited once, marked as injection points are set by the injector. For all fields the injection phase constraints have to hold as well.

The injector may use a provider method (of a binder) to create nested instances.

The injector is configured with Binders and it tracks Bindings between types (Binders and Bindings). An N4JS developer normally would not interact with this object directly except when defining an entry-point to his application. Injectors are configured with Binders which contain explicit Bindings defined by an N4JS developer. A set of these combined with implicit bindings creates the di configuration used by a given injector. To configure given Injectors with given Binder(s) use @UseBinder annotation.

11.2.1.1. DIComponent Relations

A Parent-Child relation can be established between two DIComponents. Child DIComponents use the parent bindings but can also be configured with their own bindings or change targets used by a parent. The final circumstance is local to the child and is referred to as rebinding. For more information about bindings see Binders and Bindings. A Child-Parent relation is expressed by the @WithParentInjector annotation attached to a given DIComponent. When this relation is defined between DIComponents, the user needs to take care to preserve the proper relation between injectors. In other words, the user must provide an instance of the parent injector (the injector of the DIComponent passes as a parameter to @WithParentInjector) when creating the child injector (injector of the DIComponent annotated with @WithParentInjector).

Example 99. Simple DIComponents Relation
@GenerateInjector
class ParentDIComponent{}

@GenerateInjector
@WithParentInjector(ParentDIComponent)
class ChildDIComponent{}

var parentInejctor = N4Inejctor.of(ParentDiCompoennt);
var childInjector = N4Inejctor.of(ChildDIComponent, parentInjector);

With complex DIComponent structures, injector instances can be created with a directly-declared parent and also with any of its children. This is due to the fact that any child can rebind types, add new bindings, but not remove them. Any child is, therefore, compatible with its parents.

A given DIComponent is compatible with another DIComponent if it has bindings for all keys in other component bindings.

DIC1,DIC2:DIC1.binding¯.key¯DIC2.binding¯.key¯DIC2<:DIC1
Although subtype notation <: is used here it does not imply actual subtype relations. It was used in this instance for of lack of formal notations for DI concepts and because this is similar to the Liskov Substitution principle.

A complex Child-Parent relation between components is depicted in Complex DIComponents Relations and Complex DIComponents Relations below.

diagDICParentChild
Figure 9. Complex DIComponents Relations
Example 100. Complex DIComponents Relations
@GenerateInjector class A {}
@GenerateInjector @WithParentInjector(A) class B {}
@GenerateInjector @WithParentInjector(B) class C {}
@GenerateInjector @WithParentInjector(C) class D {}
@GenerateInjector @WithParentInjector(A) class B2 {}
@GenerateInjector @WithParentInjector(B2) class C2 {}
@GenerateInjector @WithParentInjector(C2) class D2 {}
@GenerateInjector @WithParentInjector(A) class X {}
@GenerateInjector @WithParentInjector(C) class Y {}

// creating injectors
var injectorA = N4Injector.of(A);
//following throws DIConfigurationError, expected parent is not provided
//var injectorB =  N4Injector.of(B);
//correct declarations
var injectorB =  N4Injector.of(B, injectorA);
var injectorC = N4Injector.of(C, injectorB);
var injectorD = N4Injector.of(D, injectorC);
var injectorB2 = N4Injector.of(B2, injectorA);
var injectorC2 = N4Injector.of(C2, injectorB2);
var injectorD2 = N4Injector.of(D2, injectorC2);

//Any injector of {A,B,C,D,b2,C2,D2} s valid parent for injector of X, e.g. D or D2
N4Injector.of(X, injectorD);//is ok as compatible parent is provided
N4Injector.of(X, injectorD2);//is ok as compatible parent is provided

N4Injector.of(Y, injectorC);//is ok as direct parent is provided
N4Injector.of(Y, injectorD);//is ok as compatible parent is provided

N4Injector.of(Y, injectorB2);//throws DIConfigurationError, incompatible parent is provided
N4Injector.of(Y, injectorC2);//throws DIConfigurationError, incompatible parent is provided
N4Injector.of(Y, injectorD2);//throws DIConfigurationError, incompatible parent is provided

11.2.2. Binders and Bindings

Binder allows an N4JS developer to (explicitly) define a set of Bindings that will be used by an Injector configured with a given Binder. There are two ways for Binder to define Bindings: @Bind (N4JS DI @Bind) annotations and a method annotated with @Provides.

Binder is declared by annotating a class with the @Binder annotation.

A Binding is part of a configuration that defines which instance of what type should be injected into an injection point (Injection Points) with an expected type.

Provider Method is essentially a factory method that is used to create an instance of a type. N4JS allows a developer to declare those methods (see N4JS DI @Provides) which gives them a hook in instance creation process. Those methods will be used when creating instances by the Injector configured with the corresponding Binder. A provider method is a special kind of binding (key) in which the return type of the method is the key. The target type is unknown at compile time (although it may be inferred by examining the return statements of the provide method).

Definition: Binding

A binding is a pair bindkeytarget. It defines that for a dependency with a given key which usually is the expected type at the injection point. An instance of type target is injected.

A binding is called explicit if it is declared in the code, i.e. via @Bind annotation or @Provides annotation).

A binding is called implicit if it is not declared. An implicit binding can only be used if the key is a class and derived from the type at the injection point, i.e. the type of the field or parameter to be injected. In that case, the target equals the key.

A provider method M (in the binder) defines a binding

bindM.returnTypeX

(in which X is an existential type with X<:target.returnType).

For simplification, we define:

key*=target.returnType,if target is provider methodkey,otherwise (key is a type reference)

and

target*=X<:target.returnType,if target is provider methodtarget,otherwise (target is a type reference)

Req. IDE-140: Bindings (ver. 1)

For a given binding b=keytarget, the following constraints must hold: [57]

  1. key must be either a class or an interface.

  2. target must either be a class or a provider method.

  3. If b is implicit, then key must be a class. If key references a type T, then target=T – even if key is a use-site structural type.

  4. key and target* can be nominal, structural or field-structural types, either definition-site or use-site. The injector and binder needs to take the different structural reference into account at runtime!

  5. target*<:key must hold

  6. If during injection phase no binding for a given key is found, an DIUnsatisfiedBindingError is thrown.

Req. IDE-141: Transitive Bindings (ver. 1)

If an injector contains two given bindings b1=key1target1 and b2=key2key1, an effective binding b=key2target1 is derived (replacing b1).

N4JS DI mechanisms don’t allow for injection of primitives or built-in types. Only user-defined N4Types can be used. In cases where a user needs to inject a primitive or a built-in type, the developer must wrap it into its own class [58]. This is to say that none of the following metatypes can be bound: primitive types, enumerations, functions, object types, union- or intersection types. It is possible to (implicitly) bind to built-in classes.

While direct binding overriding or rebinding is not allowed, Injector can be configured in a way where one type can be separately bound to different types with implicit binding, explicit binding and in bindings of the child injectors. Binding precedence is a mechanism of Injector selecting a binding use for a type. It operates in the following order:

  1. Try to use explicit binding, if this is not available:

  2. Try to delegate to parent injectors (order of lookup is not guaranteed, first found is selected). If this is not available then:

  3. Try to use use implicit binding, which is simply to attempt to create the instance.

If no binding for a requested type is available an error will be thrown.

11.2.3. Injection Points

By injection point we mean a place in the source code which, at runtime, will be expected to hold a reference to a particular type instance.

11.2.3.1. Field Injection

In its simplest form, this is a class field annotated with @Inject annotation. At runtime, an instance of the containing class will be expected to hold reference to an instance of the field declared type. Usually that case is called Field Injection.

Req. IDE-142: Field Injection (ver. 1)

The injector will inject the following fields:

  1. All directly contained fields annotated with @Inject.

  2. All inherited fields annotated with @Inject.

  3. The injected fields will be created by the injector and their fields will be injected as well.

Example 101. Simple Field Injection

Simple Field Injection demonstrates simple field injection using default bindings. Note that all inherited fields (i.e. A.xInA) are injected and also fields in injected fields (i.e. x.y)

Simple Field Injection
class X {
    @Inject y: Y;
}
class Y {}

class A {
    @Inject xInA: X;
}
class B extends A {
    @Inject xInB: X;
}

@GenerateInjector
export public class DIC {
    @Inject a: B;
}

var dic = N4Injector.of(DIC).create(DIC);
console.log(dic);              // --> DIC
console.log(dic.a);            // --> B
console.log(dic.a.xInA);       // --> X
console.log(dic.a.xInA.y);     // --> Y
console.log(dic.a.xInB);       // --> X
console.log(dic.a.xInB.y);     // --> Y
11.2.3.2. Constructor Injection

Parameters of the constructor can also be injected, in which case this is usually referred to as Constructor Inejction. This is similar to Method Injection and while constructor injection is supported in N4JS, method injection is not (see remarks below).

When a constructor is annotated with @Inject annotation, all user-defined, non-generic types given as the parameters will be injected into the instance’s constructor created by the dependency injection framework. Currently, optional constructor parameters are always initialized and created by the framework, therefore, they are ensured to be available at the constructor invocation time. Unlike optional parameters, variadic parameters cannot be injected into a type’s constructor. In case of annotating a constructor with @Inject that has variadic parameters, a validation error will be reported. When a class’s constructor is annotated with @Inject annotation, it is highly recommended to annotate all explicitly-defined constructors at the subclass level. If this is not done, the injection chain can break and runtime errors might occur due to undefined constructor parameters. In the case of a possible broken injection chain due to missing @Inject annotations for any subclasses, a validation warning will be reported.

Req. IDE-143: Constructor Injection (ver. 1)

If a class C has a constructor marked as injection point, the following applies:

  1. If C is subclassed by S, and if S has no explicit constructor, then S inherits the constructor from C and it will be an injection point handled by the injector during injection phase.

  2. If S provides its own injector, C.ctor is no longer recognized by the injector during the injection phase. There will be a warning generated in S.ctor to mark it as injection point as well in order to prevent inconsistent injection behavior. Still, C.ctor must be called in S.ctor similarly to other overridden constructors.

11.2.3.3. Method Injection

Other kinds of injector points are method parameters where (usually) all method parameters are injected when the method is called. In a way, constructor injection is a special case of the method itself.

11.2.3.3.1. Provider

Provider is essentially a factory for a given type. By injecting an N4Provider into any injection point, one can acquire new instances of a given type provided by the injected provider. The providers prove useful when one has to solve re-injection issues since the depended type can be wired and injected via the provider rather than the dependency itself and can therefore obtain new instances from it if required. Provider can be also used as a means of delaying the instantiation time of a given type.

N4Provider is a public generic built-in interface that is used to support the re-injection. The generic type represents the dependent type that has to be obtained. The N4Provider interface has one single public method: public T get() which should be invoked from the client code when a new instance of the dependent type is required. Unlike any other unbound interfaces, the N4Provider can be injected without any explicit binding.

The following snippet demonstrates the usage of N4Provider:

class SomeService { }

@Singleton
class SomeSingletonService { }

class SomeClass {

    @Inject serviceProvider: N4Provider<SomeService>;
    @Inject singletonServiceProvider: N4Provider<SomeSingletonService>;

    void foo() {
        console.log(serviceProvider.get() ===
            serviceProvider.get()); //false

        console.log(singletonServiceProvider.get() ===
            singletonServiceProvider.get()); //true
    }

}

It is important to note that the N4Provider interface can be extended by any user-defined interfaces and/or can be implemented by any user-defined classes. For those user-defined providers, consider all binding-related rules; the extended interface, for example, must be explicitly bound via a binder to be injected. The binding can be omitted only for the built-in N4Providers.

11.2.4. N4JS DI Life Cycle and Scopes

DI Life Cycle defines when a new instance is created by the injector as its destruction is handled by JavaScript. The creation depends on the scope of the type. Aside from the scopes, note that it is also possible to implement custom scopes and life cycle management via N4JSProvider and Binder@Provides methods.

11.2.4.1. Injection Cylces

Definition: Injection Cycle

We define an injection graph GVE as a directed graph as follows: V (the vertices) is the set types of which instances are created during the injection phase and which use . E (the edges) is a set of directed and labeled edges v1v2label, where label indicates the injection point:

  1. ToTf"field", if Tf is the actualy type of an an injected field of an instance of type To

  2. TcTp"ctor", if Tp is the type of a parameter used in a constructor injection of type Tc

One cycle in this graph is an injection cycle.

When injecting instances into an object, cycles have to be detected and handled independently from the scope. If this is not done, the following examples would result in an infinite loop causing the entire script to freeze until the engine reports an error:

class A { @Inject b: B; }
class B { @Inject a: A; }
injectionGraph cycleField
Figure 10. Field Cycle
class C { @Inject constructor(d: D) {} }
class D { @Inject c: C; }
injectionGraph cycleCtorField
Figure 10. Ctor Field Cycle
class E { @Inject constructor(f: F) {} }
class F { @Inject constructor(e: E) {} }
injectionGraph cycleCtor
Figure 10. Ctor Cycle

The injector needs to detect these cycles and resolve them.

Req. IDE-144: Resolution of Injection Cycles (ver. 1)

A cycle cG, with G being an injection graph, is resolved as follows:

  1. If c contains no edge with label="ctor", the cycle is resolved using the algorithm described below.

  2. If c contains at least one edge with label="ctor", a runtime exception is thrown.

Cycles stemming from field injection are resolved by halting the creation of new instances of types which have been already created by a containing instance. The previously-created instance is then reused. This makes injecting the instance of a (transitive) container less complicated and without the need to pass the container instance down the entire chain. The following pseudo code describes the algorithm to create new instances which are injected into a newly created object:

function injectDependencies(object) {
    doInjectionWithCylceAwareness(object, {(typeof object -> object)})
}

function doInjectionWithCylceAwareness(object, createdInstancesPerType) {
    forall v $\in$ injectedVars of object {
        var type = retrieveBoundType(v)
        var instance = createdInstancesPerType.get(type)
        if (not exists instance) {
            instance = createInstance(type, createdInstancesPerType)
            doInjectionWithCylceAwareness(instance,
                createdInstancesPerType $\cap$ {(type->instance)})
        }
        v.value = instance;
    }
}

The actual instance is created in line 10 via createInstance. This function then takes scopes into account. The createdInstancesPerType map is passed to that function in order to enable cycle detection for constructor injection. The following scopes are supported by the N4JS DI, other scopes, cf. Jersey custom scopes and Guice custom scopes, may be added in the future.

This algorithm is not working for constructor injection because it is possible to already access all fields of the arguments passed to the constructor. In the algorithm, however, the instances may not be completely initialized.

11.2.4.2. Default Scope

The default scope always creates a new instance.

11.2.4.3. Singleton Scope

The singleton scope (per injector) creates one instance (of the type with @Singleton scope) per injector, which is then shared between clients.

The injector will preserve a single instance of the type of S and will provide it to all injection points where type of S is used. Assuming nested injectors without any declared binding where the second parameter is S, the same preserved singleton instance will be available for all nested injectors at all injection points as well.

The singleton preservation behavior changes when explicit bindings are declared for type S on the nested injector level. Let’s assume that the type S exists and the type is annotated with @Singleton. Furthermore, there is a declared binding where the binding’s second argument is S. In that case, unlike in other dependency injection frameworks, nested injectors may preserve a singleton for itself and all descendant injectors with @Bind annotation. In this case, the preserved singleton at the child injector level will be a different instance than the one at the parent injectors.

The tables below depict the expected runtime behavior of singletons used at different injector levels. Assume the following are injectors: C, D, E, F and G. Injector C is the top most injector and its nesting injector D, hence injector C is the parent of the injector D. Injector D is nesting E and so on. The most nested injector is G. Let’s assume J is an interface, class U implements interface J and class V extends class U. Finally assume both U and V are annotated with @Singleton at definition-site.

DI No Bindings depicts the singleton preservation for nested injectors without any bindings. All injectors use the same instance from a type. Type J is not available at all since it is not bound to any concrete implementation:

Table 12. DI No Bindings

Binding

Injector nesting (>)

C

D

E

F

G

J

NaN

NaN

NaN

NaN

NaN

U

U0

U0

U0

U0

U0

V

V0

V0

V0

V0

V0

DI Transitive Bindings is configured by explicit bindings. At the root injector level, type J is bound to type U. Since the second argument of the binding is declared as a singleton at the definition-site, this explicit binding implicitly ensures that the injector and all of its descendants preserve a singleton of the bound type U. At injector level C, D and E, the same instance is used for type J which is type U at runtime. At injector level E there is an additional binding from type U to type V that overrules the binding declared at the root injector level. With this binding, each places where J is declared, type U is used at runtime.

Furthermore, since V is declared as a singleton, both injector F and G are using a shared singleton instance of type V. Finally, for type V, injector C, D and E should use a separate instance of V other than injector level F and G because V is preserved at injector level F with the U V binding.

Table 13. DI Transitive Bindings
Binding J → U U → V

Injector nesting (>)

C

D

E

F

G

J

U0

U0

U0

V0

V0

U

U0

U0

U0

V0

V0

V

V1

V1

V1

V0

V0

DI Re - Binding depicts the singleton behaviour but unlike the above table, the bindings are declared for the interface J.

Table 14. DI Re - Binding
Binding J → U J → V

Injector nesting (>)

C

D

E

F

G

J

U0

U0

U0

V0

V0

U

U0

U0

U0

U0

U0

V

V1

V1

V1

V0

V0

DI Child Binding describes the singleton behavior when both bindings are configured at child injector levels but not the root injector level.

Table 15. DI Child Binding
Binding U V J U

Injector nesting (>)

C

D

E

F

G

J

NaN

NaN

NaN

U0

U0

U

U1

V0

V0

U0

U0

V

V1

V0

V0

V0

V0

11.2.4.4. Per Injection Chain Singleton

The per injection chain singleton is ’between’ the default and singleton scope. It can be used in order to explicitly describe the situation which happens when a simple cycle is resolved automatically. It has more effects that lead to a more deterministic behavior.

Assume a provider declared as

var pb: Provider<B>;

to be available:

@PerInjectionSingleton
class A {  }

class B { @Inject a: A; @Inject a1: A;}

b1=pb.get();
b2=pb.get();
b1.a != b2.a
b1.a == b1.a1
b2.a == b2.a1
@Singleton
class A {  }

class B { @Inject a: A; @Inject a1: A;}

b1=pb.get();
b2=pb.get();
b1.a == b2.a
b1.a == b1.a1
b2.a == b2.a1
// no annotation
class A {  }

class B { @Inject a A; @Inject a1: A;}

b1=pb.get();
b2=pb.get();
b1.a != b2.a
b1.a != b1.a1
b2.a != b2.a1

11.2.5. Validation of callsites targeting N4Injector methods

Terminology for this section:

  • a value is injectable if it

    • either conforms to a user-defined class or interface (a non-parameterized one, that is),

    • or conforms to Provider-of-T where T is injectable itself.

  • a classifier declaring injected members is said to require injection

To better understand the validations in effect for callsites targeting

N4Injector.of(ctorOfDIC: constructor{N4Object}, parentDIC: N4Injector?, ...providedBinders: N4Object)

we can recap that at runtime:

  • The first argument denotes a DIC constructor.

  • The second (optional) argument is an injector.

  • Lastly, the purpose of providedBinders is as follows:

    • The DIC above is marked with one or more @UseBinder.

    • Some of those binders may require injection.

    • Some of those binders may have constructor(s) taking parameters.

    • The set of binders described above should match the providedBinders.

Validations in effect for N4Injector.create(type{T} ctor) callsites:

  • type{T} should be injectable (in particular, it may be an N4Provider).

11.2.6. N4JS DI Annotations

Following annotations describe API used to configure N4JSDI.

11.2.6.1. N4JS DI @GenerateInjector
name

@GenerateInjector

targets

N4Class

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

NO

@GenerateInjector marks a given class as DIComponent of the graph. The generated injector will be responsible for creating an instance of that class and all of its dependencies.

11.2.6.2. N4JS DI @WithParentInjector
name

@WithParentInjector

targets

N4Class

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

TypeRef

@WithParentInjector marks given injector as depended on other injector. The depended injector may use provided injector to create instances of objects required in its object graph.

Additional WithParentInjector constraints:

Req. IDE-145: DI WithParentInjector (ver. 1)

  1. Allowed only on N4ClassDeclarations annotated with @GenerateInjector.

  2. Its parameter can only be N4ClassDeclarations annotated with .

11.2.6.3. N4JS DI @UseBinder
name

@UseBinder

targets

N4Class

retention policy

RUNTIME

transitive

NO

arguments

TypeRef

arguments are optional

NO

@UseBinder describes Binder to be used (configure) target Injector.

Req. IDE-146: DI UseInjector (ver. 1)

  1. Allowed only on N4ClassDeclarations annotated with @GenerateInjector.

  2. Its parameter can only be N4ClassDeclarations annotated with @Binder.

11.2.6.4. N4JS DI @Binder
name

@Binder

targets

N4Class

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

NONE

@Binder defines a list of bind configurations. That can be either @Bind annotations on @Binder itself or its factory methods annotated with @Provides.

Req. IDE-147: DI binder (ver. 1)

  1. Target N4ClassDeclaration must not be abstract.

  2. Target N4ClassDeclaration must not be annotated with @GenerateInjector.

  3. Target class cannot have injection points.

11.2.6.5. N4JS DI @Bind
name

@Bind

targets

N4ClassDeclaration

retention policy

RUNTIME

transitive

NO

arguments

TypeRef key, TypeRef target

arguments are optional

NO

Defines binding between type and subtype that will be used by injector when configured with target N4JS DI @Binder. See also Validation of callsites targeting N4Injector methods for description of injectable types.

Req. IDE-148: DI Bind (ver. 1)

  1. Allowed only on N4ClassDeclarations that are annotated with @Binder(N4JS DI @Binder).

  2. Parameters are instances of one of the values described in Validation of callsites targeting N4Injector methods.

  3. The second parameter must be a subtype of the first one.

11.2.6.6. N4JS DI @Provides
name

@Provides

targets

N4MethodDeclaration

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

NONE

@Provides marks factory method to be used as part DI. This is treated as explicit binding between declared return type and actual return type. This method is expected to be part of the @Binder. Can be used to implement custom scopes.

Req. IDE-149: DI Provides (ver. 1)

  1. Allowed only on N4MethodDeclarations that are part of a classifier annotated with @Binder.

  2. Annotated method declared type returns instance of one of the types described in injectable values Validation of callsites targeting N4Injector methods.

11.2.6.7. N4JS DI @Inject
name

@Inject

targets

N4Field, N4Method, constructor

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

NO

@Inject defines the injection point into which an instance object will be injected. The specific instance depends on the injector configuration (bindings) used. Class fields, methods and constructors can be annotated. See Injection Points for more information.

Req. IDE-150: DI Inject (ver. 1)

  1. Injection point bindings need to be resolvable.

  2. Binding for given type must not be duplicated.

  3. Annotated types must be instances of one of the types described in Validation of callsites targeting N4Injector methods.

11.2.6.8. N4JS DI @Singleton
name

@Singleton

targets

N4Class

retention policy

RUNTIME

transitive

NO

repeatable

NO

arguments

NO

In the case of annotating a class S with @Singleton on the definition-site, the singleton scope will be used as described in Singleton Scope.

11.3. Test Support

N4JS provides some annotations for testing. Most of these annotations are similar to annotations found in JUnit 4. For details see our Mangelhaft test framework (stdlib specification) and the N4JS-IDE specification.

In order to enable tests for private methods, test projects may define which project they are testing.

Req. IDE-151: Test API methods and types (ver. 1)

In some cases, types or methods are only provided for testing purposes. In order to improve usability, e.g. content assist, these types and methods can be annotated with @TestAPI. There are no constraints defined for that annotation at the moment.

11.4. Polyfill Definitions

In plain JavaScript, so called polyfill (or sometimes called shim) libraries are provided in order to modify existing classes which are only prototypes in plain JavaScript. In N4JS, this can be defined for declarations via the annotation @Polyfill or @StaticPolyfill. One of these annotations can be added to class declarations which do not look that much different from normal classes. In the case of polyfill classes, the extended class is modified (or filled) instead of being subclassed. It is therefore valid to polyfill a class even if it is declared @Final.

We distinguish two flavours of polyfill classes: runtime and static.

  • Runtime polyfilling covers type enrichment for runtime libraries. For type modifications the annotation @Polyfill is used.

  • Static polyfilling covers code modifications for adapting generated code. The annotation @StaticPolyfill denotes a polyfill in ordinary code, which usually provides executable implementations.

Definition: Polyfill Class

A polyfill class (or simply polyfill) is a class modifying an existing one. The polyfill is not a new class (or type) on its own. Instead, new members defined in the polyfill are added to the modified class and existing members can be modified similarly to overriding. We call the modified class the filled class and the modification filling.

We add a new pseudo property polyfill to classes in order to distinguish between normal (sub-) classes and polyfill classes.

Req. IDE-152: Polyfill Class (ver. 1)

For a polyfill class P annotated with @Polyfill or @StaticPolyfill, that is P.polyfill=true, all the following constraints must hold:

  1. P must extend a class F, F is called the filled class:

    P.super=F
  2. P’s name equals the name of the filled class and is contained in a module with same qualified name (specifier or global):

    P.name=F.nameP.containedModule.global=F.containedModule.global(P.containedModule.globalP.containedModule.specifier=F.containedModule.specifier)
  3. Both the polyfill and filled class must be top-level declarations (i.e., no class expression):

    P.topLevel=trueF.topLevel=true
  4. P must not implement any interfaces:

    P.implementedInterfaces=
  5. P must have the same access modifier (access, abstract, final) as the filled class:

    P.accessModifier=F.accessModifierP.abstract=F.abstractP.final=F.final
  6. If P declares a constructor, it must be override compatible with the constructor of the filled class:

    P.ownedCtor:P.ownedCtor<:F.ctor
  7. P must define the same type variables as the filled class F and the arguments must be in the same order as the parameters (with no further modifications):

    i,0i<|P.typePars|:P.typeParsi=F.typeParsiP.typeParsi.name=P.super.typeArgsi.name
  8. All constraints related to member redefinition (cf. Redefinition of Members) have to hold. In the case of polyfills, this is true for constructors (cf. [Req-IDE-72]) and private members.

11.4.1. Runtime Polyfill Definitions

(Runtime) Libraries often do not provide completely new types but modify existing types. The ECMA-402 Internationalization Standard [ECMA12a], for example, changes methods of the built-in class Date to be timezone aware. Other scenarios include new functionality provided by browsers which are not part of an official standard yet. Even ECMAScript 6 [ECMA15a] extends the predecessor [ECMA11a] in terms of new methods (or new method parameters) added to existing types (it also adds completely new classes and features, of course).

Runtime polyfills are only applicable to runtime libraries or environments and thus are limited to n4jsd files.

Req. IDE-153: Runtime Polyfill Class (ver. 1)

For a runtime-polyfill class P annotated with @Polyfill, that is P.staticpolyfill=false, all the following constraints must hold in addition to [Req-IDE-152]:

  1. Both the polyfill and filled class are provided by the runtime (annotated with @ProvidedByRuntime): [59]

P.providedByRuntime=trueF.providedByRuntime=true

Req. IDE-154: Applying Polyfills (ver. 1)

A polyfill is automatically applied if a runtime library or environment required by the current project provides it. In this case, the following constraints must hold:

  1. No member must be filled by more than one polyfill.

11.4.2. Static Polyfill Definitions

Static polyfilling is a compile time feature to enrich the definition and usually also the implementation of generated code in N4JS. It is related to runtime polyfilling described in Runtime Polyfill Definitions in a sense that both fillings enrich the types they address. Despite this, static polyfilling and runtime polyfilling differ in the way they are handled.

Static polyfills usually provide executable implementations and are thus usually found in n4js files. However, they are allowed in n4jsd files, as well, for example to enrich generated code in an API project.

The motivation for static polyfills is to support automatic code generation. In many cases, automatically generated code is missing some information to make it sufficiently usable in the desired environment. Manual enhancements usually need to be applied. If we think of a toolchain, the question may arise how to preserve the manual work when a regeneration is triggered. Static polyfilling allows the separation of generated code and manual adjustments in separate files. The transpiler merges the two files into a single transpiled file. To enable this behaviour, the statically fillable types must be contained in a module annotated with @StaticPolyfillAware. The filling types must also be annotated with @StaticPolyfill and be contained in a different module with same specifier but annotated with @StaticPolyfillModule. Static polyfilling is restricted to a project, thus the module to be filled as well as the filling module must be contained in the same project.

We add a new pseudo property staticPolyfill to classes in order to distinguish between normal (sub-) classes and static polyfill classes. We add two new pseudo properties to modules in order to modify the transpilation process. The mutually-exclusive properties staticPolyfillAware and staticPolyfill signal the way these files are processed.

In order to support efficient transpilation, the following constraint must hold in addition to constraints:

Req. IDE-155: Static Polyfill Layout (ver. 1)

For a static polyfill class P annotated with @StaticPolyfill, that is P.staticpolyfill=true, all the following constraints must hold in addition to [Req-IDE-152]:

  1. P’s name equals the name of the filled class and is contained in a module with the same qualified name:

    P.name=F.nameP.containedModule.specifier=F.containedModule.specifier
  2. Both the static polyfill and the filled class are part of the same project:

    P.project=F.project
  3. The filled class must be contained in a module annotated with @StaticPolyfillAware:

    F.containedModule.staticPolyfillAware=true
  4. The static polyfill and the filled type must both be declared in an n4js file or both in an n4jsd file.

  5. The filling class must be contained in a module annotated with @StaticPolyfillModule:

    P.containedModule.staticPolyfillModule=true
  6. For a statically-filled class F there is at most one static polyfill:

    P1is static polyfill ofFP2is static polyfill ofFP1=P2

Req. IDE-156: Restrictions on static polyfilling (ver. 1)

For a static polyfilling module MP the following must hold:

  1. All top-level elements are static polyfills:

    TMPT.topLevel=trueT.staticPolyfill=true
  2. It exists exactly one filled module MF annotated with staticPolyfillAware in the same project.

  3. It is an error if two static polyfill modules for the same filled module exist in the same project:

    M1.specifier=M2.specifierM1.project=M2.project M1.staticPolyfillModul=M2.staticPolyfillModul=trueM1=M2
Example 102. Static polyfill

Static Polyfill, Genmod shows an example of generated code. Static Polyfill, Polyfillmod demonstrates the static polyfill.

Note that the containing project has two source folders configured:
Project/src/n4js and Project/src/n4jsgen.

Static Polyfill, Polyfillmod
@@StaticPolyfillAware
export public class A {
    constructor() {...}
    m1(): void{...}
}
export public class B {
    constructor() {...}
    m2(): void{...}
}
Static Polyfill, Genmod
@@StaticPolyfillModule
@StaticPolyfill
export public class B extends B {
    @Override
    constructor(){ ... } // replaces generated ctor of B
    @Override
    m1(): void {...} // adds overridden method m1 to B
    @Override
    m2(): void {...} // replaces method m2 in B
    m3(): void {...} // adds new method m3 to B
}

11.4.3. Transpiling static polyfilled classes

Transpiling static polyfilled classes encounters the special case that two different n4js source files with the same qualified name are part of the project. Since the current transpiler is file-based, both files would be transpiled to the same output destination and would therefore overwrite each other. The following pre-transpilation steps handle this situation:

  • Current file to transpile is M

  • If M.staticPolyfillAware=true, then

    • search for a second file G with same qualified name:
      G.specifier=M.specifierG.project=M.project

    • If G, then

      • merge G into current file MM'

      • conventionally transpile M'

    • else conventionally transpile M

  • else, if M.staticPolyfillModule=true,

    • then do nothing. (Transpilation will be triggered for filled type separately.)

  • else, conventionally transpile M

Quick Links