Global scoping

Xtext has a default mechanism for global scoping that allows you to refer to elements defined in a different file, possibly in a different project of the workspace; in particular, it uses the dependencies of the Eclipse projects. For Java projects, it uses the classpath of the projects. Of course, this mechanism relies on the global index.

Note

Global scoping is implied by the fact that the default scoping mechanism always relies on an outer scope that consists of the visible object descriptions in the index.

With the default configuration in the MWE2, this mechanism for global scoping works out of the box. You can experiment with a project with some SmallJava files. You will see that you can refer to the SmallJava classes defined in another file; content assist works accordingly.

Before proceeding to the use of global scoping, it is worthwhile to learn how to write JUnit tests that concern several input programs.

As hinted in the section The index, when running in a plain Java context where there is no workspace concept, the index is based on an EMF ResourceSet. There is an overloaded version of ParseHelper.parse that also takes the resource set to be used when loading the passed program string. Thus, if we want to write a test that involves several files where one of them refers to elements of the other, we need to parse all input strings using the same resource set.

This can be accomplished in two ways. You can inject a Provider<ResourceSet>, create a resource set through this provider, and pass it to the parse method as follows (we write this test in the SmallJavaValidatorTest):

@Inject Provider<ResourceSet> resourceSetProvider;
@Test def void testTwoFiles() {
  val resourceSet = resourceSetProvider.get
  val first = '''class B extends A {}'''.parse(resourceSet)
  val second = '''class A { B b; }'''.parse(resourceSet)
  first.assertNoErrors
  second.assertNoErrors

  second.classes.head.assertSame(first.classes.head.superclass)
}

Note that in this test, the two input programs have mutual dependencies and the cross-reference mechanism works, since we use the same resource set.

Note

It is crucial to validate the models only after all the programs are loaded; remember that ParseHelper only parses the program, it does not try to resolve cross-references.

Alternatively, you can parse the first input and retrieve its resource set from the returned model object; then, the subsequent inputs are parsed using that resource set:

@Test def void testTwoFilesAlternative() {
  val first = '''class B extends A {}'''.parse
  val second = '''class A { B b; } '''.
        parse(first.eResource.resourceSet)
   ... as before

In the following sections, we will implement some aspects related to global scoping and the index.

Packages and imports

Since we can refer to elements of other files, it might be good to introduce the notion of namespace in SmallJava, which corresponds to the Java notion of a package.

Thus, we add an optional package declaration in the rule for SJProgram:

SJProgram:
  ('package' name=QualifiedName ';')?
  classes+=SJClass*;

QualifiedName: ID ('.' ID)* ;

The rule QualifiedName is a data type rule. A data type rule is similar to a terminal rule, for example, the terminal rule for ID, and it does not contain feature assignments. Differently from a terminal rule, a data type rule is valid only in specific contexts, that is, when it is used by another rule. A data type rule is executed by the parser, which has a much more sophisticated lookahead strategy than the lexer that executes the terminal rules. This way, it will not conflict with terminal rules; for example, the rule QualifiedName will not conflict with the rule ID.

Tip

The Xtext editor highlights a data type rule's name in blue.

According to the default mechanism for computing a qualified name (see the section Qualified names), when a SJClass is contained in a program with a package declaration, its fully qualified name will include the package name. For example, given this program:

package my.pack;
class C { }

The class C will be stored in the index with the qualified name my.pack.C.

It now makes sense to allow the user to refer to a SmallJava class with its fully qualified name, like in Java.

When you specify a cross-reference in an Xtext grammar, you can use the complete form [<Type>|<Syntax>], where <Syntax> specifies the syntax for referring to the element of that type. The compact form [<Type>] we used so far is just a shortcut for [<Type>|ID]. In fact, until now, we have always referred to elements by their ID. Now, we want to be able to refer to SJClass using the QualifiedName syntax; thus, we modify all the involved rules accordingly. We show the modified rule for SJClass, but also the rules for SJTypedDeclaration and SJNew must be modified accordingly:

SJClass:
  'class' name=ID ('extends' superclass=[SJClass|QualifiedName])?
  '{' members += SJMember* '}' ;

The rule for QualifiedName also accepts a single ID, thus, if there is no package, everything keeps on working as before. This means that all existing tests should still be successful.

We can now test class references with qualified names in separate files:

@Test def void testPackagesAndClassQualifiedNames() {
  val first = '''
  package my.first.pack;
  class B extends my.second.pack.A {}
  '''.parse
  val second = '''
  package my.second.pack;
  class A {
    my.first.pack.B b;
  }
  '''.parse(first.eResource.resourceSet)
  first.assertNoErrors
  second.assertNoErrors

  second.classes.head.assertSame(first.classes.head.superclass)
}

Now, it would be nice to have an import mechanism as in Java so that we can import a class by its fully qualified name once and then refer to that class simply by its simple name. Similarly, it would be helpful to have an import with wildcard * in order to import all the classes of a specific package. Xtext supports imports, even with wildcards; it only requires that a feature with name importedNamespace is used in a parser rule and then the framework will automatically treat that value with the semantics of an import; it also handles wildcards as expected:

SJProgram:
  ('package' name=QualifiedName ';')?
  imports+=SJImport*
  classes+=SJClass*;

SJImport:
    'import' importedNamespace=QualifiedNameWithWildcard ';' ;

QualifiedNameWithWildcard: QualifiedName '.*'? ;

The following test verifies imports:

@Test def void testImports() {
  val first = '''
  package my.first.pack;
  class C1 { }
  class C2 { }'''.parse

  '''
  package my.second.pack;
  class D1 { }
  class D2 { }'''.parse(first.eResource.resourceSet)
        
  '''
  package my.third.pack;
  import my.first.pack.C1;
  import my.second.pack.*;
  
  class E extends C1 { // C1 is imported
    my.first.pack.C2 c; // C2 not imported, but fully qualified
    D1 d1; // D1 imported by wildcard
    D2 d2; // D2 imported by wildcard
  }
  '''.parse(first.eResource.resourceSet).assertNoErrors
}

To keep the SmallJava DSL simple, we do not require the path of the .smalljava file to reflect the fully qualified name of the declared class as in Java. Indeed, in SmallJava, all classes are implicitly public and can be referred by any other SmallJava class.

The index and the containers

The index does not know anything about visibility across resources. In fact, the index is global in that respect. The Xtext index can also be seen as the counterpart of the JDT indexing mechanism for all the Java types, and the Eclipse platform indexing mechanism that keeps track of all the files in all the projects in the workspace. JDT provides the dialog "Open Type" that can be accessed using the menu Navigate | Open Type or with the shortcut Ctrl + Shift + T. This allows you to quickly open any Java type that is accessible from the workspace. Eclipse provides the dialog "Open Resource" that can be accessed using the shortcut Ctrl + Shift + R. This allows you to quickly open any file in the workspace. Xtext provides a similar dialog, "Open Model Element" that can be accessed by navigating to the Navigate | Open Model Element menu or with the shortcut Ctrl + Shift + F3. This allows you to quickly open any Xtext DSL element that is in the index, independently from the project. An example is shown in the next screenshot, where the dialog provides you quick access to all the elements of all the DSLs we implemented so far:

The index and the containers

The mechanism concerning the visibility across resources is delegated to IContainer that can be seen as an abstraction of the actual container of a given resource. The inner class IContainer.Manager provides information about the containers that are visible from a given container. Using these containers, we can retrieve all the object descriptions that are visible from a given resource.

Note

The implementation of the containers and the managers depends on the context of execution. In particular, when running in Eclipse, containers are based on Java projects. In this context, for an element to be referable, its resource must be on the classpath of the caller's Java project and it must be exported. This allows you to reuse for your DSL all the mechanisms of Eclipse projects, and the users will be able to define dependencies in the same way as they do when developing Java projects inside Eclipse. We refer to the About the index, containers, and their manager section of the Xtext documentation for all the details about available container implementations.

The procedure to get all the object descriptions, which are visible from a given EObject o consists of the following steps:

  1. Get the index.
  2. Retrieve the resource description of the object o.
  3. Use the IContainer.Manager instance to get all the containers in the index that are visible from the resource description of o.
  4. Retrieve the object descriptions from the visible containers, possibly filtering them by type.

We thus add some utility methods to the class SmallJavaIndex:

class SmallJavaIndex {
  ...
  @Inject IContainer.Manager cm
  ...
  def getVisibleEObjectDescriptions(EObject o, EClass type) {
    o.getVisibleContainers.map[ container |
      container.getExportedObjectsByType(type)
    ].flatten
  }

  def getVisibleClassesDescriptions(EObject o) {
    o.getVisibleEObjectDescriptions
      (SmallJavaPackage.eINSTANCE.SJClass)
  }

  def getVisibleContainers(EObject o) {
    val index = rdp.getResourceDescriptions(o.eResource)
    val rd = index.getResourceDescription(o.eResource.URI)
    cm.getVisibleContainers(rd, index)
  }...

Note that the result of map in the preceding code is a List<Iterable<IEObjectDescription>>; the flatten utility method from the Xtend library combines multiple iterables into a single one. Thus, the final result will be an Iterable<IEObjectDescription>.

In the section Exported objects, we created the class SmallJavaIndex with utility methods to retrieve all the descriptions exported by a resource. We used those methods to write learning tests to get familiar with the index. The methods we have just added to SmallJavaIndex will be effectively used in the rest of the chapter to perform specific tasks that require access to all the visible elements. In particular, getVisibleClassesDescriptions will be useful for checking duplicate classes across files, in the next section.

Checking duplicates across files

The visibility of elements is implemented in the scope provider; thus, usually the index is not used directly. One of the scenarios where you must use the index is when you want to check for duplicates across files in a given container, that is, in a project and all its dependencies. The validator for SmallJava currently only implements checks for duplicates in a single resource.

We now write a validator method to check for duplicates across files using the index. We only need to check instances of SJClass, since they are the only globally visible objects.

Note

Do not traverse the resources in the resource set (that is, visit all of them) since this is an expensive operation. Instead, use the index in these situations since this is both better and cheaper. It is OK to visit elements in the resource being processed.

The idea is to use the method SmallJavaIndex.getVisibleClassesDescriptions to get all the object descriptions of the type SJClass that are visible from the resource of a given SmallJava class and search for duplicate qualified names. These descriptions include both the elements stored in other resources and the one exported by the resource under validation. Thus, it is essential to compute the difference, so that we collect only the descriptions, corresponding to SJClass elements that are defined in resources different from the one under validation. To this aim, we add the following method to SmallJavaIndex:

def getVisibleExternalClassesDescriptions(EObject o) {
  val allVisibleClasses = o.getVisibleClassesDescriptions
  val allExportedClasses = o.getExportedClassesEObjectDescriptions
  val difference = allVisibleClasses.toSet
  difference.removeAll(allExportedClasses.toSet)
  return difference.toMap[qualifiedName]
}

To compute such difference, we first transform the descriptions to sets and use the Set.removeAll method. We also transform the result into a Map, where the key is the description's qualified name. This allows us to quickly find a description given a qualified name.

In the validator, it is just a matter of checking that for each SJClass c in the SJProgram there is no element in the preceding map with the same qualified name of c:

@Inject extension SmallJavaIndex
@Inject extension IQualifiedNameProvider
public static val DUPLICATE_CLASS =
  ISSUE_CODE_PREFIX + "DuplicateClass"

// perform this check only on file save
@Check(CheckType.NORMAL)
def checkDuplicateClassesInFiles(SJProgram p) {
    val externalClasses = p.getVisibleExternalClassesDescriptions
    for (c : p.classes) {
      val className = c.fullyQualifiedName
      if (externalClasses.containsKey(className)) {
        error("The type " + c.name + " is already defined",
          c,
          SmallJavaPackage.eINSTANCE.SJNamedElement_Name,
          DUPLICATE_CLASS)
      }
    }
  }
}

Note that we specified the CheckType.NORMAL in the @Check annotation; this instructs Xtext to call this method only on file save, not during editing as it happens normally (the default is CheckType.FAST). This is a good choice since this check might require some time, and if executed while editing, it might reduce the editor performance. Eclipse JDT also checks for class duplicates across files only on file save.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.81.206