javaxtextscopingcross-referencexbase

Error "Cyclic linking detected" while calling a referenced object in a ScopeProvider


I am currently implementing cross-referencing for my Xtext dsl. A dsl file can contain more then one XImportSection and in some special case an XImportSection does not necessariely contain all import statements. It means I need to customize the "XImportSectionNamespaceScopeProvider" to find/build the correct XimportSection. During the implementation I figured out an unexpected behavior of the editor and/or some validation.

I used the following dsl code snipped for testing my implementation:

delta MyDelta {
    adds {
        package my.pkg;
        import java.util.List;
        public class MyClass 
                implements List
                                {
        } 
    }
    modifies my.pkg.MyClass { // (1)

        adds import java.util.ArrayList;
        adds superclass ArrayList<String>;
    }
}

The dsl source code is described by the following grammar rules (not complete!):

AddsUnit:
    {AddsUnit} 'adds' '{' unit=JavaCompilationUnit? '}';

ModifiesUnit:
    'modifies' unit=[ClassOrInterface|QualifiedName] '{'
    modifiesPackage=ModifiesPackage?
    modifiesImports+=ModifiesImport*
    modifiesSuperclass=ModifiesInheritance?
    '}';

JavaCompilationUnit:
    => (annotations+=Annotation*
    'package' name=QualifiedName EOL)?
    importSection=XImportSection?
    typeDeclarations+=ClassOrInterfaceDeclaration;

ClassOrInterfaceDeclaration:
    annotations+=Annotation* modifiers+=Modifier* classOrInterface=ClassOrInterface;

ClassOrInterface: // (2a)
    ClassDeclaration | InterfaceDeclaration | EnumDeclaration | AnnotationTypeDeclaration;

ClassDeclaration: // (2b)
    'class' name=QualifiedName typeParameters=TypeParameters?
    ('extends' superClass=JvmTypeReference)?
    ('implements' interfaces=Typelist)?
    body=ClassBody;

To provide better tool support, a ModifiesUnit references the class which is modified. This Xtext specific implementation enables hyperlinking to the class.

I am currently working on customized XImportSectionScopeProvider which provides all namespace scopes for a ModifiesUnit. The default implemantation contain a method protected List<ImportNormalizer> internalGetImportedNamespaceResolvers(EObject context, boolean ignoreCase) assumes that there is only one class-like element in a source file. But for my language there can be more then one. For this reason I have to customize it.

My idea now is the following implementation (using the Xtend programming language):

override List<ImportNormalizer> internalGetImportedNamespaceResolvers(EObject context, boolean ignoreCase) {
    switch (context) {
        ModifiesUnit: context.buildImportSection
        default: // ... anything else
    }
}

Before I startet this work, the reference worked fine and nothing unexpected happend. My goal now is to build a customized XImportSection for the ModifiesUnit which is used by Xbase to resolve references to JVM types. To do that, I need a copy of the XImportSection of the referenced ClassOrInterface. To get access to the XImportSection, I first call ModifiesUnit.getUnit(). Directly after this call is executed, the editor shows the unexpected behaviour. The minimal implementation which leads to the error looks like this:

def XImportSection buildImportSection(ModifiesUnit u) {
    val ci = u.unit // Since this expression is executed, the error occurs!
    // ...
}

Here, I don't know what is going internally! But it calculates an error. The editor shows the follwoing error on the qualified name at (1): "Cyclic linking detected : ModifiesUnit.unit->ModifiesUnit.unit".

My questions are: What does it mean? Why does Xtext show this error? Why does it appear if I access the referenced object?

I also figured out a strange thing there: In my first approach my code threw a NullPointerException. Ok, I tried to figure out why by printing the object ci. The result is:

org.deltaj.scoping.deltaJ.impl.ClassOrInterfaceImpl@4642f064 (eProxyURI: platform:/resource/Test/src/My.dj#xtextLink_::0.0.0.1.1::0::/2)
org.deltaj.scoping.deltaJ.impl.ClassDeclarationImpl@1c70366 (name: MyClass)

Ok, it seems to be that this method is executed two times and Xtext resolves the proxy between the first and second execution. It is fine for me as long as the received object is the correct one once. I handle it with an if-instanceof statement.

But why do I get two references there? Does it rely on the ParserRule ClassOrInterface (2a) which only is an abstract super rule of ClassDeclaration (2b)? But why is Xtext not able to resolve the reference for the ClassOrInterface?


Solution

  • OK, now I found a solution for my problem. During I was experimenting with my implementation, I saw that the "Problems" view stil contained unresolved references. This was the reason to rethink what my implementation did. At first, I decided to build the returned list List<ImportNormalizer directly instead of building an XImportSection which then will be converted to this list. During implementing this, I noticed that I have built the scope only for ModifiesUnitelements instead of elements which need the scope within a ModifiesUnit. This is the reason for the cyclic linking error. Now, I am building the list only if it is needed. The result is that the cyclic linking error occurs does not occur any more and all references to JVM types are resolved correctly without any errors in the problems view.

    My implementation now looks like this:

    class DeltaJXImportSectionNamespaceScopeProvider extends XImportSectionNamespaceScopeProvider {
    
        override List<ImportNormalizer> internalGetImportedNamespaceResolvers(EObject context, boolean ignoreCase) {
    
            // A scope will only be provided for elements which really need a scope. A scope is only necessary for elements
            // which are siblings of a JavaCompilationUnit or a ModifiesUnit.
            if (context.checkElement) { // (1)
                return Collections.emptyList
            }
    
            // Finding the container which contains the import section
            val container = context.jvmUnit // (2)
    
            // For a non null container create the import normalizer list depending of returned element. If the container is
            // null, no scope is needed.
            return if (container != null) { // (3)
                switch (container) {
                    JavaCompilationUnit: container.provideJcuImportNormalizerList(ignoreCase)
                    ModifiesUnit: container.provideMcuImportNormalizerList(ignoreCase)
                }
            } else {
                Collections.emptyList
            }
    
        }
    
        // Iterates upwards through the AST until a ModifiesUnit or a JavaCompilationUnit is found. (2)
        def EObject jvmUnit(EObject o) {
            switch (o) {
                ModifiesUnit: o
                JavaCompilationUnit: o
                default: o.eContainer.jvmUnit
            }
        }
    
        // Creates the list with all imports of a JCU (3a)
        def List<ImportNormalizer> provideJcuImportNormalizerList(JavaCompilationUnit jcu, boolean ignoreCase) {
            val is = jcu.importSection
            return if (is != null) {
                is.getImportedNamespaceResolvers(ignoreCase)
            } else {
                Collections.emptyList
            }
        }
    
        // Creates the list of all imports of a ModifiesUnit. This implementation is similar to 
        // getImportedNamespaceResolvers(XImportSection, boolean) // (3b)
        def List<ImportNormalizer> provideMcuImportNormalizerList(ModifiesUnit mu, boolean ignoreCase) {
            val List<ImportNormalizer> result = Lists.newArrayList
            result.addAll((mu.unit.jvmUnit as JavaCompilationUnit).provideJcuImportNormalizerList(ignoreCase))
            for (imp : mu.modifiesImports) {
                if (imp instanceof AddsImport) {
                    val decl = imp.importDeclaration
                    if (!decl.static) {
                        result.add(decl.transform(ignoreCase))
                    }
                }
            }
            result
        }
    
        // Creates an ImportNormalizer for a given XImportSection
        def ImportNormalizer transform(XImportDeclaration decl, boolean ignoreCase) {
            var value = decl.importedNamespace
            if (value == null) {
                value = decl.importedTypeName
            }
            return value.createImportedNamespaceResolver(ignoreCase)
        }
    
        // Determines whether an element needs to be processed. (1)
        def checkElement(EObject o) {
            return o instanceof DeltaJUnit || o instanceof Delta || o instanceof AddsUnit || o instanceof ModifiesUnit ||
                o instanceof RemovesUnit
        }
    }
    

    As one can see, elements which do not need namespaces for correct scopes, will be ignored (1).

    For each element which might need namespace for a correct scope the n-father element which directly contains the imports is determined (2).

    With the correct father element the namespace list can be calculated (3) for JCU's (3a) and MU's (3b).