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.
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.
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.
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
.
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 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 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.
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:
o
.IContainer.Manager
instance to get all the containers in the index that are visible from the resource description of o
.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.
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.
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.
3.134.81.206