Search code examples
eclipsedslxtextscopingxtend

Scoping complex QName (recursive grammar)


The grammar:

grammar sample.types.Types with org.eclipse.xtext.common.Terminals

generate types "http://www.types.sample/Types"

Model:
   structs    +=Struct+
   data       +=Data+
   assignments+=Assignment+
;
Struct:
   'struct' name=ID '{' fields+=Field+ '}'
;
Field:
   ( scalar=Scalar | composite=[Struct|ID] ) name=ID
;
Data:
   type=[Struct|ID] name=ID
;
Assignment:
   qname=QName '=' value=Value
;
QName returns Ref:
   DataRef ( {QName.ref=current} '->' field=[Field|ID] )+
;
DataRef returns Ref:
   {DataRef} data=[Data|ID]
;
Scalar:
     'number'
   | 'string'
;
Value:
     INT
   | STRING
;

An instance of text with two errors (intentionnaly):

 1 struct SampleA {
 2    number num
 3    string str
 4 }
 5 struct SampleB {
 6    number  num
 7    SampleA aaa
 8 }
 9
10 SampleA sampleA1
11 SampleA sampleA2
12 SampleB sampleB
13
14 sampleA1->num = 12
15 sampleA1->str = "Hello"
16 sampleA1->aaa->num = 22        // Must be an error because aaa
17                                // is not a field of struct SampleA
18
19 sampleA2->num = 12
20 sampleA2->str = "Hello"
21
22 sampleB->num      = 42
23 sampleB->aaa->num = 55         // Ctrl-clic on 'num' must go to line #2
24 sampleB->aaa->str = "Yes!"
25 sampleB->str      = "toto"     // Must be an error because str
                                  // is not a field of struct SampleB

The xtend scoping class:

package sample.types.scoping

import org.eclipse.emf.ecore.EObject
import org.eclipse.emf.ecore.EReference
import org.eclipse.xtext.scoping.IScope
import org.eclipse.xtext.scoping.Scopes
import sample.types.types.DataRef
import sample.types.types.QName
import sample.types.types.Struct
import sample.types.types.TypesPackage

class TypesScopeProvider extends AbstractTypesScopeProvider {

   override IScope getScope( EObject context, EReference reference ) {
      if( reference === TypesPackage.Literals.QNAME__FIELD ) {
         if( context instanceof QName ) {
            val head = context.ref
            switch( head ) {
               DataRef: return Scopes::scopeFor( head.data.type.fields )
               QName: {
                  val tail = head.field
                  switch( tail ) {
                     Struct: return Scopes::scopeFor( tail.composite.fields )
                  }
               }
               default: return IScope::NULLSCOPE
            }
         }
      }
      super.getScope( context, reference )
   }
}

The errors in the sample text are:

  • at lines 23 : sampleB->aaa->num = 55: Couldn't resolve reference to Field 'num'.
  • at lines 24 : sampleB->aaa->str = "Yes!": Couldn't resolve reference to Field 'str'.

How to improve the scoping class to recurse into composite fields?

Thanks to Christian, here is the final version of the Scoping class:

package sample.types.scoping

import org.eclipse.emf.ecore.EObject
import org.eclipse.emf.ecore.EReference
import org.eclipse.xtext.scoping.IScope
import org.eclipse.xtext.scoping.Scopes
import sample.types.types.Composite
import sample.types.types.DataRef
import sample.types.types.QName
import sample.types.types.TypesPackage

class TypesScopeProvider extends AbstractTypesScopeProvider {

   override IScope getScope( EObject context, EReference reference ) {
      if( reference === TypesPackage.Literals.QNAME__FIELD ) {
         if( context instanceof QName ) {
            val head = context.ref
            switch( head ) {
               DataRef: if( head?.data?.type?.fields !== null ) {
                  return Scopes::scopeFor( head.data.type.fields )
               }
               QName: {
                  val tail = head.field
                  switch( tail ) {
                     Composite: if( tail?.composite?.fields !== null ) {
                        return Scopes::scopeFor( tail.composite.fields )
                     }
                  }
               }
               default: return IScope::NULLSCOPE
            }
         }
      }
      super.getScope( context, reference )
   }
}

Solution

  • I propose to use a recursive grammar in this case as descibed in https://www.dietrich-it.de/xtext/2013/05/18/xtext-and-dot-expressions.html

    DotExpression returns Ref:
        EntityRef ({DotExpression.ref=current}  "." tail=[Feature])*
    ;
    

    you can find a port of the scope provider to the new one that does not inherit from AbstractDeclarativeScopeProvider here https://www.eclipse.org/forums/index.php?t=msg&th=1074397&goto=1722095&#msg_1722095

    override getScope(EObject context, EReference reference) {
        if (reference == MyDslPackage.Literals.DOT_EXPRESSION__TAIL) {
            if (context instanceof DotExpression) {
                val head = context.ref;
                switch (head) {
                    EntityRef:
                        return Scopes::scopeFor(head.entity.features)
                    DotExpression: {
                        val tail = head.tail
                        switch (tail) {
                            Attribute: return IScope::NULLSCOPE
                            Reference: return Scopes::scopeFor(tail.type.features)
                            default: return IScope::NULLSCOPE
                        }
                    }
                    default:
                        return IScope::NULLSCOPE
                }
            }
        }
    
        return super.getScope(context, reference)
    }
    

    in your case this would look like

    class MyDslScopeProvider extends AbstractMyDslScopeProvider {
        override IScope getScope(EObject context, EReference reference) {
            if (reference === MyDslPackage.Literals.QNAME__FIELD) {
                if (context instanceof QName) {
                    val head = context.ref
                    switch ( head ) {
                        DataRef: {
                            return Scopes::scopeFor(head.data.type.fields)
                        }
                        QName: {
                            val tail = head.field
                            if (tail.composite !== null) {
                                return Scopes::scopeFor(tail.composite.fields)
                            }
                        }
                        default:
                            return IScope::NULLSCOPE
                    }
                }
            }
            super.getScope(context, reference)
        }
    }