This post is about some juggling with technologies in the EMF ecosystem, namely XText and XCore. Consider a small (and not completely realistic) DSL for defining entity types (like the introductory grammar example of the XText documentation). Often there are two different kinds of types in such a language:
- primitive/native types, provided by the system, e.g., String, Integer, Boolean, …
- composite/complex types, which have been modeled in the language and can have attributes of primitive type or refer to other complex types
So we might model these variants of properties as Attribute
for primitive types and Association
for complex types…
Since we would like to have a nice and expressive meta model, we use generics to define it (in XCore):
interface EntityModel { contains Type[] types } interface Nameable { String name } interface Type extends Nameable {} class PrimitiveType extends Type { BaseType baseType } class ComplexType extends Type { contains Property<?>[] properties } interface Property<T extends Type> extends Nameable { refers T ^type } class Attribute extends Property<PrimitiveType> {} class Association extends Property<ComplexType> {} enum BaseType { String Integer Boolean }
The corresponding XText grammar could look like this:
EntityModel: (types += Type)*; Type: PrimitiveType | ComplexType; PrimitiveType: 'data' name=ID: baseType = BaseType; BaseType: String | Integer | Boolean; ComplexType: 'class' name=ID ('{' (properties += Property)+ '}')?; Property: Attribute | Association; Attribute: 'attribute' name=ID ':' [PrimitiveType]; Association: 'association' name=ID ':' [ComplexType];
So now if we generate the eclipse plugin and start it in a new eclipse instance, we may write something like this:
data Text: String type Animal { attribute name: Text association favorite: Food } type Food { attribute name: Text }
And the AST (abstract syntax tree or semantic model as the XText documentation call it) looks like this:
A nice feature of XText is content assist. That means, if you hit ctrl+Space the editor offers you possibilities what you might want to enter next. One assistance type is showing referenceable elements which are in scope. That means, it offers you available primitive types for attributes and complex types for associations. But this feature is not aware of the generics we used above.
Consider, we enter in line 4 (see the highlighting above) of our data model example association favorite:
. The editor creates a meta model object as soon as the first assigned action in the corresponding grammar matched. In our case it is an object of class Association
with name=favorite
. If we now hit ctrl+Space, the scoping mechanism of XText is invoked and it provides the scope provider with a context element (the current meta model object… in our case the favorite association ) as well as a reference for which the scope should be evaluated.
Although class Association
tackles/defines the type parameter T extends Type
of Property
to ComplexType
the attribute type
is not declared in Association
and its type is implicitly narrowed in Association
together with type parameter T
. Hence, the attributes type is mistakenly evaluated to Type
and not to ComplexType
. The default semantic of the scope mechanism is to retrieve all objects matching the type of the reference. Therefore, the editor will provide Food and Text (the same holds for Attribute
s). If we choose Text
over Food
, the editor tries to add a value of PrimitiveType
to the attribute type
, but the meta model allows ComplexType
, only. This fails and results in a validation error message.
An editor should not present you a code completion that is just wrong, since this is just wrong! Fortunately, we can add our own implementation (in XTend) of IScopeProvider
and “fix” the default behavior:
class DataScopeProvider extends AbstractDataScopeProvider { override getScope(EObject context, EReference reference) { switch context { Association case reference == EntityModelPackage.Literals.PROPERTY__TYPE: { val rootElement = EcoreUtil2.getRootContainer(context) val candidates = EcoreUtil2.getAllContentsOfType(rootElement, ComplexType) Scopes.scopeFor(candidates) } Attribute case reference == EntityModelPackage.Literals.PROPERTY__TYPE: { val rootElement = EcoreUtil2.getRootContainer(context) val candidates = EcoreUtil2.getAllContentsOfType(rootElement, PrimitiveType) Scopes.scopeFor(candidates) } default: super.getScope(context, reference) } } }
In line 4 and 9 we add the partitioning type check for Association
and Attribute
regarding our scope provider. This way the default behavior is overwritten to decide the scope via the generic association type
only. In lines 6 and 11 the elements to propose as content assistance are added (either all ComplexType
s or PrimitiveType
s defined in the same file).
Exciting 🤓