Xtext cross references and scoping – an overview (Part 3)

Overview

Default Imports

Motivation

A question regularly asked is how to implement built in types for your DSL. Enums or particular keywords for those types, i.e. hard coding them in the grammar, is possible, but the library approach is considered good (if not best) practice. The basic idea is shipping a model file containing the built in types along with the rest of the language infrastructure, as usually it is possible to model them just like a user defined type.

Of course, it would be nice if those types were visible without the user having to explicitly import that model file or the corresponding namespace.

The Approach

The solution proposed here assumes that the importing project has a “dependency” to the jar containing the default model (for want of a better term). That is, the default model is on the classpath of the importing model. One possibility is that the default model is contained in a “library” plugin project and the importing plugin has a dependency to the former.

URI Imports

If you are using the import uri mechanism, i.e. you are explicitly importing a particular model file, you can hook into the ImportUriGlobalScopeProvider. You simply add the default imports to the set of imports picked up from the model itself

public class MyImportUriGlobalScopeProvider extends
    ImportUriGlobalScopeProvider {

  @Override
  protected LinkedHashSet<URI> getImportedUris(EObject context) {
    LinkedHashSet<URI> temp = super.getImportedUris(context);
    temp.add(URI.createURI("classpath:/the/library/package/defaults.mydsl"));
    return temp;
  }
}

You could make this change known analogous to the way the ImportUriGlobalScopeProvider is bound in the AbstractXRuntimeModule

@Override
public void configureIScopeProviderDelegate(com.google.inject.Binder binder) {
  binder.bind(org.eclipse.xtext.scoping.IScopeProvider.class).
  annotatedWith(com.google.inject.name.Names.named(
  "org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider.delegate
  )).to(MyImportUriGlobalScopeProvider.class);}

Namespace Imports

When using namespace imports (so that an element can be referenced via its simple name rather than the fully qualified one) the hook is to be found in the ImportedNamespaceAwareLocalScopeProvider. In Xtext 1.x something like the following snippet does the trick.

public class MyImportedNamespaceAwareLocalScopeProvider extends
    ImportedNamespaceAwareLocalScopeProvider {

  @Override
  protected Set<ImportNormalizer> getImportNormalizer(EObject context) {
    Set<ImportNormalizer> temp = super.getImportNormalizer(context);
    temp.add(new ImportNormalizer(
      new QualifiedName("builtin.types.namespace.*")
    ));
    return temp;
  }
}

In Xtext 2 there is a dedicated method for implicit imports (also the QualifiedName-API has changed).

public class MyImportedNamespaceAwareLocalScopeProvider extends
    ImportedNamespaceAwareLocalScopeProvider {

  @Override
  protected List<ImportNormalizer> getImplicitImports(boolean ignoreCase) {
    List<ImportNormalizer> temp=new ArrayList();
    temp.add(new ImportNormalizer(
      QualifiedName.create("builtin","types","namespace"),
      true, ignoreCase));
    return temp;
  }
}

In both cases the change is made know in the runtime module as above, now binding MyImportedNamespaceAwareLocalScopeProvider.

It should be clear that you can also use this approach for dealing with “split packages”, i.e. spreading the same namespace over several files. Usually, you would have to import “your own” namespace in order to refer to elements from another file using their simple names. Note that the getImplicitImports method in the Xtext 2 example is not the right hook now, as there you don’t have access to the context element (for obtaining its namespace).

2 Responses to “Xtext cross references and scoping – an overview (Part 3)”

  1. Beastie Says:

    Really helpful article, thanks!

    I have such question regarding the example about ImportedNamespaceAwareLocalScopeProvider :

    On one hand I want to be able to validate weather imports are existing types which is why I have the following grammar definition:

    DomainModel:
    packageDeclaration=PackageDeclaration;

    PackageDeclaration:
    “package” name=QualifiedName “;” importDeclarations+=ImportDeclaration*
    abstractTypeDeclaration=AbstractTypeDeclaration;

    ImportDeclaration:
    “import” importedNamespace=[TypeDeclaration|QualifiedName] “;”;

    AbstractTypeDeclaration:
    modifier+=ClassOrInterfaceModifier* typeDeclaration=TypeDeclaration;

    TypeDeclaration:
    EnumDeclaration | InterfaceDeclaration
    ;

    EnumDeclaration:
    name=ID …
    ;
    InterfaceDeclaration:
    name=ID ….
    ;

    Withing the current file I want to be able to reference types that are only imported with import statement using their simple names instead of their FQNs. Which is why I am using your example here. I’ve added another method which have to dynamically build List based on the imports of the current file. I am using this:

    @Override
    protected List internalGetImportedNamespaceResolvers(
    EObject context, boolean ignoreCase) {
    if (!(context instanceof DomainModel)) {
    return Collections.emptyList();
    }

    List importedNamespaces = Lists.newArrayList();
    DomainModel domainModel = (DomainModel) context;
    EList importDeclarations = domainModel.getPackageDeclaration().getImportDeclarations();
    for(ImportDeclaration importDeclaration : importDeclarations) {
    String value = importDeclaration.getImportedNamespace();
    ImportNormalizer resolver = createImportedNamespaceResolver(value, ignoreCase);
    if(resolver !=null) {
    importedNamespaces.add(resolver);
    }
    }

    return importedNamespaces;
    }

    The not so cool thing here is that if the importedNamespace references a type (as is in my grammar) I cannot extract the string representation of it. So this line won’t be possible in my case:
    String value = importDeclaration.getImportedNamespace();
    I am getting an object of type TypeDeclaration but nowhere in this object is present the import itself.
    On the other hand if I change the importedNampespace definition to importedNamepsace=QualifiedName I won’t be able to verify and navigate to the imported statements.

    I want to be able to verify my imports and in the same time I want to reference only imported namespaces in my file.

    Do you have any thoughts about that? Am I doing something wrong?

    Thanks a lot in advance!

  2. Beastie Says:

    I managed to make it work. Sorry it was stupid thing I couldn’t think of by the time I wrote the comment :)

Leave a Reply