Scoping

An IScopeProvider is responsible for providing an IScope for a given context EObject and EReference (declared or inherited by the object’s EClass). The returned IScope should contain all target candidates for the given object and cross reference.

public interface IScopeProvider {

  /**
   * Returns a scope for the given context. The scope provides access to 
   * the compatible visible EObjects for a given reference.
   *
   * @param context the element from which an element shall be referenced
   * @param reference the reference to be used to filter the elements.
   * @return {@link IScope} representing the inner most {@link IScope} for 
   *         the passed context and reference.
   */
  public IScope getScope(EObject context, EReference reference);

  /**
   * Returns a scope for a given context. The scope contains any visible, 
   * type-compatible element.
   * @param context the element from which an element shall be referenced
   * @param type the (super)type of the elements.
   * @return {@link IScope} representing the inner most {@link IScope} for
   *         the passed context and type.
   */
  public IScope getScope(EObject context, EClass type);
}

A single IScope represents an element of a linked list of scopes. That means that a scope can be nested within an outer scope. For instance Java has multiple kinds of scopes (object scope, type scope, etc.).

For Java one would create the scope hierarchy as commented in the following example:

// file contents scope
import static my.Constants.STATIC;

public class ScopeExample { // class body scope
  private Object field = STATIC;

  private void method(String param) { // method body scope
    String localVar = "bar";
    innerBlock: { // block scope
      String innerScopeVar = "foo";
      Object field = innerScopeVar;
      // the scope hierarchy at this point would look like this:
      //  blockScope{field,innerScopeVar}->
      //  methodScope{localVar, param}->
      //  classScope{field}-> ('field' is overlayed)
      //  fileScope{STATIC}->
      //  classpathScope{'all qualified names of accessible static fields'} ->
      //  NULLSCOPE{}
      //
    }
    field.add(localVar);
  }
}

In fact the class path scope should also reflect the order of class path entries. For instance:

classpathScope{stuff from bin/}
-> classpathScope{stuff from foo.jar/}
-> ...
-> classpathScope{stuff from JRE System Library}
-> NULLSCOPE{}

Please find the motivation behind this and some additional details in this blog post .

The default implementation would produce this hierarchy of scopes for the model from the last example in the previous chapter:

//file model.dsl
import "model1.dsl";
import "model2.dsl";
 
ref Foo;
entity Bar;

//file model1.dsl 
entity Stuff;

//file model2.dsl
entity Foo;

Scope (model.dsl) {
  parent : Scope (model1.dsl) {
    parent : Scope (model2.dsl) {}
  }
}

When enumerating the scope’s content, the first, most specialized scope would return Bar, its parent would provide Stuff and the outermost scope adds Foo. The linker will iterate the scope in that order and abort when it finds a matching ScopedElement.

Declarative Scope Provider

As always there is an implementation that allows to specify scoping in a declarative way (extend AbstractDeclarativeScopeProvider for this purpose). It looks up methods which have either of the following two signatures:

IScope scope_<ContextReference>(<ContextType> ctx, EReference ref)

IScope scope_<TypeToReturn>(<ContextType> ctx, EClass type)

The former is used when evaluating the scope for a specific cross reference and here <ContextReference> corresponds to the name of this reference (prefixed with the name of the reference’s declaring type and separated by an underscore). The ref parameter represents this cross reference.

The latter method signature is used when computing the scope for a given element type and is applicable to all cross references of that type. Here <TypeToReturn> is the name of that type which also corresponds to the type parameter.

So if you for example have a state machine with a Transition object owned by its source State and you want to compute all reachable states (i.e. potential target states), the corresponding method could be declared as follows (assuming the cross reference is declared by the Transition type and is called target):

IScope scope_Transition_target(Transition this, EReference ref)

If such a method does not exist, the implementation will try to find one for the context object’s container. Thus in the example this would match a method with the same name but State as the type of the first parameter. It will keep on walking the containment hierarchy until a matching method is found. This container delegation allows to reuse the same scope definition for elements in different places of the containment hierarchy. Also it may make the method easier to implement as the elements comprising the scope are quite often owned or referenced by a container of the context object. In the example the State objects could for instance be owned by a containing StateMachine object.

If no method specific to the cross reference in question was found for any of the objects in the containment hierarchy, the implementation will start looking for methods matching the other signature (with the EClass parameter). Again it will first attempt to match the context object. Thus in the example the signature first matched would be:

IScope scope_State(Transition this, EClass type)

If no such method exists, the implementation will again try to find a method matching the context object’s container objects. In the case of the state machine example you might want to declare the scope with available states at the state machine level:

IScope scope_State(StateMachine this, EClass type)

This scope can now be used for any cross references of type State for context objects owned by the state machine.