3. Names

Visibility defines the scope in which a declaration is visible, that is in which context references can be bound to the declaration. Access control defines the extent to which types and members are accessible beyond their immediate context. Access control may, therefore, restrict the visibility of a declaration by limiting its scope.

Extensibility refers to whether a given type can be subtyped, or in the case of members, whether they can be overridden. Access control is a prerequisite for extensibility which is further explained in N4JS Specific Classifiers

3.1. Access Control

Types from one project may or may not be made accessible to another project. Likewise, members from a given type may or may not be made accessible to members existing outside that type. For example, if a developer writes an application which uses a library, which types within that library can the application see? Given a type that is set as visible, which members of that type can the application see?

Accessing a type or member actually means that a reference is bound to a declaration with the same identifier.

We distinguish the following contexts from which an element is accessed as follows:

  1. Module or type: access from elements in the same module or type.

  2. Subtype: access from a subtype.

  3. Project: access from the same project.

  4. Vendor: access from different project of the same vendor.

  5. World: access from anything else.

Accessibility is defined by modifiers on types and members, e.g public, protected, project, private, via the export statement, and by the @Internal annotation. Extensibility is defined by the @Final annotation respectively.

3.2. Accessibility of Types, Top-Level Variables and Function Declarations

We define types (classes, interfaces, enums) whereby each type has members (fields and methods, depending on the kind of type). When we define a type, we need to define whether it is visible only for the specifying module, project or whether that type should be accessible from outside of that project.

The same is true for variable declarations and function declarations defined as top-level elements of a module.

The following type access modifiers are supported by N4JS:

enum TypeAccessModifier:      project
                            | public;

If a type is not exported, its visibility is private. If a type has declared visibility public, it may additionally be marked as internal via the annotation @Internal. Thus, we have the following set of type access modifiers:

TAM = private project public@Internal public

That is, in N4JS, only the type access modifiers and are available. The redundant project modifier serves only documentation purpose and can be synthesized if the export modifier is preset.

All other modifiers used here are synthesized as shown in the next example:

Synthesized Type Access Modifiers in N4JS
class C0 {}                         // private
export class C1 {}                  // project
export project class C1 {}          // project
export @Internal public class C1 {} // public@Internal
export public class C2 {}           // public

var v0;                             // private
export var v1;                      // project
export project var v1;              // project
export @Internal public var v3;     // public@Internal
export public var v2;               // public


function f0() {}                    // private
export function f1() {}             // project
export project function f1() {}     // project
export @Internal public function f3() {} // public@Internal
export public function f2() {}      // public

The access control levels are defined as listed in Type Access Control.

Table 1. Type Access Control
Type Access

Modifier

Module

Project

Vendor

World

private

yes

no

no

no

project

yes

yes

no

no

public@Internal

yes

yes

yes

no

public

yes

yes

yes

yes

TAM is a totally ordered set:

private<project<public@Internal<public

Req. IDE-3: Type Access Modifiers (ver. 1)

The following constraints for type access modifiers for a given type T (which may be a classifier declaration, a function or a variable) must hold:

  • It is an error if T is not exported but defined as project, public or public@Internal.

  • It is an error if an annotation @Internal is present on a module private or project visible type.

  • The type modifier for all built-in ECMAScript types is public.

  • The default modifier for user declared exported declarations is project. That is, this modifier is assumed if no modifier is explicitly specified.

Definition: Type Accessibility T

The function αT:TypeReference×TypeDeclarationBoolean computes whether a given type, (top-level) variable or function reference can access the declaration that it references. αT is defined with Type Access Control.

Formally, we define αT for a given reference r and a module top level variable, function or type declaration D as follows [4]:

D.acc=publicαTrD
D.acc=public@Internalr.vendor=D.vendorαTrD
D.acc=projectr.project=D.projectαTrD
D.acc=privater.module=D.moduleαTrD

If the type of the arguments is clear from the context, we simply write αrD instead of αTrD.

Accessibility for types is only checked for types that manifest themselves in the concrete syntax of the N4JS file. Types that do not have to be written to concrete syntax may be used even if they are generally not accessible. This is illustrated by Implicit, allowed type references in N4JS:

Example 1. Implicit, allowed type references in N4JS
export public class D {
    public takeC(): C { .. }
    public acceptC(c: C): void { .. }
}
/* private */ class C {}
var d: D = new D()
d.acceptC( d.takeC() )

3.2.1. Accessibility of Members

Accessibility at the member level is only applicable when the type itself is accessible. If you cannot access the type, you cannot access any of its members. Note that inherited members (from an interface or class) become members of a class. For example, if B extends A, and if A is not accessible to some client C but B is, then the members of A are indirectly accessible to C in so far as they are accessed via B. This is true in particular for interfaces, as their properties are possibly merged into the consuming class (cf. Implementation of Members).

The following member access modifiers are supported by N4JS:

enum MemberAccessModifier:    private
                            | project
                            | protected
                            | public;

The modifiers protected and public may be annotated with @Internal. Thus, we can define the following set of member access modifiers:

MAM={private,protected@Internal,protected,project,public@Internal,public}

protected@Internal and public@Internal are synthesized tags and were introduced as shorthand notation for the @Internal annotation together with protected or public access modifiers. The project modifier is the default one and it can be omitted. As with the type access modifiers, not all member access modifiers are available in N4JS. Instead, they are synthesized from different construct as shown in the next example.

Example 2. Synthesized Member Access Modifiers in N4JS
export @Internal public class C {

    private f0;                 // private
    f1;                         // project
    project f2;                 // project
    @Internal protected f3;     // protected@Internal
    protected f4;               // protected
    @Internal public f5;        // public@Internal
    public f6;                  // public

    private m0() {}             // private
    m1() {}                     // project
    project m2() {}             // project
    @Internal protected m3() {} // protected@Internal
    protected m4() {}           // protected
    @Internal public m5() {}    // public@Internal
    public  m6() {}             // public
}

MAM does not define a totally ordered set. However, its subset

MAM\public@Internal

is a totally ordered set [5] :

private<project<protected@Internal<protected<public

Member Access Control shows which members are accessible from where.

Table 2. Member Access Control
Access Modifier Inside Module Inside Project Vendor Vendor Subtypes Other Projects Everywhere

private

yes

no

no

no

no

no

project

yes

yes

no

no

no

no

protected@Internal

yes

yes

yes

no

no

no

protected

yes

yes

yes

no

yes

no

public@Internal

yes

yes

yes

yes

no

no

public

yes

yes

yes

yes

yes

yes

We define the relation

=:TAM×MAM

as follows:

=::={privateprivate,projectproject,public@Internalpublic@Internal,publicpublic}

We further define the relation :TAM×MAM as follows:

mam'MAM:tam=mam'mam'mamtammam

Less, greater then etc. are defined accordingly.

Definition: Member Accessibility

The function

αm:MemberReference×MemberDeclarationBoolean

computes if a given reference can access the member declaration that it references.

Note that αm and bind are different functions. A reference can only bind to a declaration if it can access the declaration. However, bind requires more condition to work (correct metatypes, no shadowing etc).

Formally, we define αm for a given reference r and member declaration M as follows: [6] [7]

M.acc=publicαmrM
r.vendor=M.vendorM.acc=public@InternalαmrM
r.ownerr.receiver.super*M.acc=protectedαmrM
r.ownerr.receiver.super*r.vendor=M.vendorM.acc=protected@InternalαmrM
r.project=M.projectM.acc=projectαmrM
r.module=r.moduleM.acc=privateαmrM

If the type of the arguments is clear from the context, we simply write αrM instead of αmrM.

Although private members are accessible inside a module, it is not possible to redefine (override etc.) these members (see Redefinition of Members).

Req. IDE-4: Default Member Access Modifiers (ver. 1)

The following constraints for member access modifiers must hold:

  1. The default modifier for members of user-declared classes is project.

  2. The default modifier for members of interfaces is the same as the visibility of the interface itself, except for private interfaces. For private interfaces, the default modifier for members is project.

  3. The modifier for enum literals is always public.

  4. Private members of a classifier are visible and accessible within a module, i.e. you can access the private method of a class, for instance, when the use of the class as receiver is in the same module where the class has been defined. In case of inheritance, private members are visible if the host (e.g. the class) is in the same module as the provider (the extended class). This also means that abstract members of a class are allowed to be defined private as they may be overridden within a module.

Example 3. Type and Member Access Modifiers
export project interface I {
    project foo();
}

// This interface may be used publicly, but since the inherited method foo() is project visible only,
// it is not possible to implement that interface in other projects.
export public interface J extends I {
}

// Since the visibility of foo is set to public here, it is possible to implement this interface in other projects.
export public interface K extends I {
    @Override public foo();
}

// Since foo is private, it is not possible to subclass the class in other modules. Still, it
// is possible to use it in other projects.
// XPECT noerrors -->
export public abstract class C {
    private abstract foo();

    public static C instance() {
        // return some default instance
        ...
    }
}

As demonstrated in the following snippet, class C can be used but not subclassed in other modules:

import C from "C"

// XPECT errors --> "Cannot extend class C: cannot implement one or more non-accessible abstract members: method C.foo." at "C"
export public abstract class Sub extends C {
}

// XPECT noerrors -->
var c: C = C.instance();

Members of non-visible types are, in general, not visible for a client. Members may become visible, however, if they are accessed via a visible type which inherits these members. The following examples demonstrate two different scenarios:

Example 4. Declaring type vs receiver type

It is especially noteworthy that the declaring type of a member is generally not considered for the accessibility of that member but only the receiver type is relevant.

class Base {
    public m(b: Base): void {}
}
export public class ApiType extends Base {
}
import * as N from "Base";

var t = new N.ApiType();
// member can be accessed although type Base is not exported:
t.m(t);

The property access to the member m is valid because it fulfills the constraints for accessibility. The receiver of the property access is t of type ApiType. That type is exported and accessible. Therefore, the inherited member m is also considered valid since it is also defined public.

This rule allows for defining a common functionality in module or project visible types that becomes accessible via exported, visible subtypes.

Example 5. Member Access and Type Access Interplay

The following example demonstrates the behavior when non-visible types are used as return types. In this case, all the members of the non-visible types are not accessible, even if they have a public access modifier.

class A {
    foo(): void{}
}
export public class C {
    public getHidden(): A { return new A() };
}
import * as Nfrom "A"

class Client {
    f(): void {
        var c = new N.C();
        // XPECT noerrors --> Getting an instance the hidden type is possible
        var hidden = c.getHidden();
        // XPECT errors --> "The method foo is not visible." at "foo"
        hidden.foo();
    }
}

3.2.2. Valid Names

For identifier and property names, the same constraints as in ECMAScript [ECMA11a(p.S7.6)] [ECMA11a(p.S7.6.1.2)] [ECMA11a(p.S11.6)] are applied.

Identifier names in N4JS are defined similar to [ECMA11a(p.S11.6)], making it possible to even use reserved words (keywords etc.). For some element types, errors or warnings are issued in order to prevent problems when using these names.

Req. IDE-5: Forbidden Identifier Names in N4JS (ver. 1)

In N4JS mode, errors are generated in the following cases:

  1. A name of a type equals

    1. an access modifier

    2. set or get

    3. an ECMAScript keyword

    4. a boolean literal

    5. the name of a base type

  2. The name of a function or function expression equals (but not the method)

    1. an ECMAScript keyword

    2. a reserved future ECMAScript word

Req. IDE-6: Undesired Identifier Names in N4JS (ver. 1)

In N4JS mode, warnings are generated in the following cases:

  1. The name of a member (of a non external type)

    1. equals the name of a base type [8] but the type of the variable is different from that type

    2. is not static nor const but starts with an upper case letter

  2. The name of a non-external N4 types (class, interface, enum) starts with a lower case letter

  3. The name of a variable (incl. formal parameter or catch variable and fields)

    1. equals an N4JS keyword

    2. equals the name of a base type but the type of the variable is different from that type

    3. is not const but starts with an upper case letter

3.2.3. Qualified Names

In N4JS source code, types can only be referenced using their simple name. There is no such thing as a fully-qualified type name in N4JS or ECMAScript. Types are uniquely identified by their simple name, maybe together with an import and the module specifier given there. Clashes between simple names of imported type and locally declared types can be resolved by importing the type under an alias.

In some cases, however, we need to define references to types or even members. For example, if we want to reference certain members in JSDoc comments or for unambiguous error messages. For this reason, we formally define qualified names even if they cannot occur in source code.

Different forms of module and type specifiers. shows the different names of a given type C, defined in a module M.n4js, defined in a package p of a project MyProject.

Simple type names are used throughout N4JS code in order to refer to types. The different forms of module specifiers are only used in import declarations in the string following the from keyword.

Table 3. Different forms of module and type specifiers.
Name Example

Simple Type Name

C

(Plain) Module Specifier

p/M

Complete Module Specifier

MyProject/p/M

Complete Type Specifier

MyProject/p/M.C

3.2.4. Name Duplicates

There might be cases where two (or more) scopes created by different entities with the same (simple) name overlap. Those situations can be referred to as shadowing, hiding, or obscuring. While they are not the same, many of those cases are not allowed in N4JS. For simplicity we refer to them all as shadowing or duplication (see below). Rule of thumb is that N4JS allows everything that is allowed in JavaScript StrictMode.

3.2.4.1. Lexical Environment

N4JS handles scopes similar to ECMAScript, so that function scope is applied to variables declared with var (and parameters), and block scope for variables is declared with let or const. In general, ECMAScript defines Lexical Environments as a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code [ECMA11a(p.10.2)].

Elements that introduce lexical environments:

FunctionDefinition, VariableDeclaration, CatchBlock, WithStatement, ImportDeclaration

N4JS specific declarations:

N4ClassDeclaration, N4InterfaceDeclaration, N4EnumDeclaration, N4MethodDeclaration.

Additionally, a built-in lexical environment that defines global scope exists for every Script.

Since N4JS is extended (and a bit more strict) JS strict mode, Object environment records created by WithStatement are not taken into account when resolving duplicates. This applies to both N4JS mode and JS strict mode. In unrestricted JS the WithStatement is allowed but duplicates are not validated.

In case of names introduced by ImportDeclarations only NamedImportSpecifierss are taken into account (their import name or its alias if available). WildcardImportSpecifierss are not taken into account. Potential optimizations by compiler or user annotation are also not currently taken into account during analysis.
3.2.4.2. Duplicates and Shadowing

Two elements with the same name declared in the same lexical environment (cf. [ECMA11a(p.S10.2.2.1)] are called duplicates. An element defined in an environment shadows all elements with the same name in outer environments.

In class hierarchies, a member with the same name as a member defined in a supertype is said to override the latter. Overriding is discussed in Redefinition of Members.

For the following constraints, we make the following assumptions:

  • Names of function expressions or declarations are handles similar to locally declared elements in the function. Function declarations are additionally declaring a name in their outer scope.

  • The implicit formal parameter arguments is treated similar to declared formal parameters.

  • Formal parameters are defined in the lexical environment of a function, that is, they are defined in the same lexical environment as local var-variables or other declarations in that function.

  • The "global" environment contains objects globally defined by the execution environment.

Req. IDE-7: Forbidden Duplicates (ver. 1)

There must be no two elements defined in the same lexical environment with the same name, that is, there must be no duplicates.

Req. IDE-8: Forbidden Shadowing (ver. 1)

In general, shadowing is allowed in N4JS. But it is not allowed in the following cases:

  1. No element defined in the standard global scope must be shadowed.

  2. There must be no function shadowing another function.

  3. Elements defined in catch blocks must not shadow elements defined all parent non-catch-block environments.

Req. IDE-9: Forbidden Names (ver. 1)

  1. In the script environment, it is not allowed to use the name ’arguments’.[9]

    Forbidden Shadowing shows nested lexical environments with named elements declared inside (all named x here), the forbidden cases are marked with arrows (the numbers at the left side refer to the numbers in [Req-IDE-8].

shadowing
Figure 1. Forbidden Shadowing

Rationale:

  • We expect only few named nested functions. Since this is expected to be a rare case, no shadowing should occur there as this is maybe not expected by the programmer.

  • It is typical that nested environments define local variables. In particular helper variables (such as i: number i or s: string ) are expected to be used quite often. Since this is a typical case, we allow shadowing for local variables.

  • Function declarations may shadow type declarations. However, both entities are to be handled completely differently, so that an error will occur if the shadowing is ignored by the programmer anyway.

Quick Links