Re: PsiElementFinder implementation not called to resolve types from other package

Updated thread title 01/05/2014 to reflect findings since initial post


==================

Hello community,

I am working on a plugin to integrate an Annotation Processor with IDEA.
This annotation processor generates several classes from the annoted ones.

Since IDEA does not compile source unless explictly told to, we need to tell IDEA about those not-yet-exising classes and I found that it is the exact purpose of PsiElementFinder.

I wrote an implementation of PsiElementFinder and as far as I can tell, it correctly generates PsiClass objects for each class which is generated by the annotation processor (returned value of each methods are consistent and the PsiElement tree of the generated PsiClass looks legitimate).

But, when I run a Plugin Test and open a file with references to the generated classes (as class property types and method parameter types) they never get resolved. Text of the type stay red, hitting CTRL+SPACE does not display any import option nor does it call my PsiElementFinder implementation. In fact, the only case where I see my PsiElementFinder beeing called in DEBUG is when I modify my class code and type an invalid assignment to a property of a generated class type.

I must have done something wrong because my understanding is that providing PsiClass to resolve missing type imports is the primary goal of PsiElementFinder and I can't make my implementation do just that.

Thanks in advance for any input on the subject.

Seb'

Here is an a abstract of the class I use to test my plugin :

[...imports, but none of the TeacherToPeopleMapper type...]

public class PeopleIndexServiceImpl implements PeopleIndexService {
    private final TeacherRository teacherRepository;
    private final TeacherToPeopleMapper teacherToPeopleMapper; // TeacherToPeopleMapper is a generated class

    public PeopleIndexServiceImpl(TeacherRository teacherRepository,
                                  TeacherToPeopleMapper teacherToPeopleMapper) {
        this.teacherRepository = teacherRepository;
        this.studentRepository = studentRepository;
        // the following statement references an inexistant property (it misses an 'r'), when I remove that 'r' my PsiElementFinder implementation is called         this.teacherToPeopleMappe = teacherToPeopleMapper;     } }


Here is my PsiElementFinder implementation of which I removed implementation details which are not related to IDEA:

[... imports ...]

public class MyElementFinder extends PsiElementFinder {

  @Nullable
  @Override
  public PsiClass findClass(@NotNull String qualifiedName, final @NotNull GlobalSearchScope scope) {
    String mapperFile = extractMapperQualifiedName(qualifiedName, searchedClassType);
    PsiClass psiClass = retrieveSourceclass(mapperFile, scope);
    if (psiClass == null) {
      LOGGER.debug(String.format("Class %s not found", mapperFile));
      return null;
    }

    if (!hasMapperAnnotation(psiClass)) {
      LOGGER.debug(String.format("Class %s is not annoted with @Mapper", mapperFile));
      return null;
    }

    String text = createClassText(psiClass);

    PsiJavaFile psiJavaFile = (PsiJavaFile) PsiFileFactory.getInstance(project).createFileFromText(
                                                                computeFileName(mapperFile),
                                                                JavaFileType.INSTANCE,
                                                                text
                                                            );
    return psiJavaFile.getClasses()[0];
  }

  private String createClassText(PsiClass psiClass) {
    [... kind of parsing Psi tree to generate the Java source of the generated class ...]
  }

  private boolean hasMapperAnnotation(PsiClass psiClass) {
    [...]
  }

  private boolean computeFileName(String mapperFile) {
    [...]
  }

  private PsiClass retrieveSourceclass(String mapperFile, GlobalSearchScope scope) {
    PsiFile[] filesByName = PsiShortNamesCache.getInstance(scope.getProject()).getFilesByName(mapperFile + ".java");
    if (filesByName.length == 0) {
      return null;
    }
    Optional first = FluentIterable.from(Arrays.asList(filesByName)).filter(PsiJavaFile.class).first();
    if (first.isPresent()) {
      return first.get().getClasses()[0];
    }
    return null;
  }

  @NotNull
  @Override
  public PsiClass[] findClasses(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
    PsiClass aClass = findClass(qualifiedName, scope);
    if (aClass == null) {
      return PsiClass.EMPTY_ARRAY;
    }
    return new PsiClass[]{aClass};
  }

  @Nullable
  @Override
  public PsiPackage findPackage(@NotNull String qualifiedName) {
    return super.findPackage(qualifiedName); // use super method because we do not generate any new package
  }

  @NotNull
  @Override
  public PsiPackage[] getSubPackages(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope) {
    return super.getSubPackages(psiPackage, scope); // use super method because we do not generate any new package
  }

  @NotNull
  @Override
  public PsiClass[] getClasses(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope) {

    List res = Lists.newArrayList();
    for (PsiDirectory dir : psiPackage.getDirectories(scope)) {
      if (dir.getVirtualFile().getFileType() == FileTypes.ARCHIVE) {
        continue;
      }
      for (PsiFile psiFile : dir.getFiles()) {
        if (!(psiFile instanceof PsiJavaFile)) {
          continue;
        }

        PsiClass[] classes = ((PsiJavaFile) psiFile).getClasses();
        if (classes.length == 0) { // current psiFile may be package-info.java which does not contain classes, ignore it
          continue;
        }

        res.addAll(Arrays.asList(classes));

        PsiClass psiClass = classes[0]; // assuming the first class if the public class
        if (!hasMapperAnnotation(psiClass)) {
          // class is not annoted, ignore it
          continue;
        }

        String text = createClassText(psiClass);

        PsiJavaFile psiJavaFile = (PsiJavaFile) PsiFileFactory.getInstance(project).createFileFromText(
                                                                    computeFileName(mapperFile),
                                                                    JavaFileType.INSTANCE,
                                                                    text
                                                                );
        return psiJavaFile.getClasses()[0];
      }
    }

    return res.toArray(new PsiClass[res.size()]);
  }

  @NotNull
  @Override
  public Set getClassNames(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope) {
    return super.getClassNames(psiPackage, scope
    ); // super method uses getClasses(PsiPackage, GlobalSearchScope) in a way that suits us
  }

  @Override
  public boolean processPackageDirectories(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope,
                                           @NotNull Processor consumer) {
    return super.processPackageDirectories(psiPackage, scope, consumer
    ); // don't know what's that method for, use supermethod for now
  }

  @NotNull
  @Override
  public PsiClass[] getClasses(@Nullable String className, @NotNull PsiPackage psiPackage,
                               @NotNull GlobalSearchScope scope) {
    return super.getClasses(className, psiPackage, scope
    ); // super method uses getClasses(PsiPackage, GlobalSearchScope) in a way that suits us
  }
}
4 comments

Hello community,

Too bad nobody could help me on this one, but I've keeped on working on it anyway. I created a very simple new plugin and narrowed down the problem.

It seems that either :

  • I am expecting from the PsiElementFinder something it is not meant to do
  • I am not implementing it correctly to do what I want
  • or implementing PsiElementFinder is not enough (and something else should be written/implemented/whatever)


What I am expecting :

  • when typing a reference to a generated/class, IDEA suggests the import of that class
  • when typing a qualified reference (in code or as an import), IDEA is ok (class name is not red)


What I observe :

  • referencing a generated class/interface from a class in the same package as the generated class/interface : it works
  • referencing a generated class/interface from another class in another package : no suggestion for imports, class name is red in import statement and code reference


A quick refresh of what I am trying to achieve and how :

  • I am working on a framework generating Java classes and interfaces from Java classes annoted with a specific annotation using an AnnotationProcessor
  • these generated Java classes and interfaces can be used in any class of the project
  • I understand that by implementing a PsiElementFinder I can make IDEA aware of these news classes without actually building the project (therefor making usage of the framework much more convenient and fluent/seamless in IDEA)
  • I am using PsiFileFactory.createFileFromText to create PsiClass objects for the generated classes and interfaces


The new plugin I created, from scratch, with a new PsiElementFinder is as simple as possible (see attached file TestPlugin.zip). It contains :

  • a basic plugin.xml file
  • a PsiElementFinder implementation which :
    • uses hardcoded source to generate a Class C
    • generate a class C in package fr.javatronic.testplugin.sample.sub (no more condition on a specific annotation on a class)
    • implements findClass, findClasses and getClasses


I created a sample project to test the plugin (see attached file sample-project-testplugin.zip) and to reproduce the problem where source is organised as follow :

package fr.javatronic.testplugin;

import java.util.Arrays;
import java.util.List;
import com.google.common.collect.Lists;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.psi.JavaDirectoryService;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElementFinder;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiPackage;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class TestPluginElementFinder extends PsiElementFinder {

  @Nullable
  @Override
  public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
    if ("C".endsWith(qualifiedName)) {
      return getPsiClass(scope);
    }
    return null;
  }

  @NotNull
  @Override
  public PsiClass[] findClasses(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
    PsiClass aClass = findClass(qualifiedName, scope);
    if (aClass == null) {
      return PsiClass.EMPTY_ARRAY;
    }
    return new PsiClass[] { aClass };
  }

  @NotNull
  @Override
  public PsiClass[] getClasses(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope) {
    if (psiPackage.getQualifiedName().startsWith("fr.javatronic.testplugin.sample.sub")) {
      List res = Lists.newArrayList();
      for (PsiDirectory dir : psiPackage.getDirectories(scope)) {
        PsiClass[] classes = JavaDirectoryService.getInstance().getClasses(dir);

        res.addAll(Arrays.asList(classes));
        res.add(getPsiClass(scope));
      }
      return res.toArray(new PsiClass[res.size()]);
    }
    return super.getClasses(psiPackage, scope);
  }

  private static final String C_SOURCE = "package fr.javatronic.testplugin.sample.sub;\n" +
      "\n" +
      "public class D {\n" +
      "}";

  private PsiClass getPsiClass(GlobalSearchScope scope) {
    PsiJavaFile psiJavaFile = (PsiJavaFile) PsiFileFactory.getInstance(scope.getProject())
                                                          .createFileFromText(
                                                              "C.java",
                                                              JavaFileType.INSTANCE,
                                                              C_SOURCE
                                                          );
    return psiJavaFile.getClasses()[0];
  }

}


Here is the code of class A in the test project :

package fr.javatronic.testplugin.sample;

import fr.javatronic.testplugin.sample.sub.C; // C is not resolved

public class A {
  private C c; // C is neither resolved nor suggested for import by IDEA

}


Here is the code of class B in the test project :

package fr.javatronic.testplugin.sample.sub;

public class B {
  private C c; // C is resolved
}
0

Hi Sébastien,

I haven't had time to read all the detail you provided, sorry, but here are some suggestions you might investigate, this is my understanding of what's required for virtual classes:

  1. PsiElementFinder - looks like you have this.
  2. GotoClassContributor (extends ChooseByNameContributor) - so you can navigate to elements by name.
  3. PsiShortNamesCache - this is required to find classes, fields and methods from their short (non-qualified) names.
  4. QueryExecutors for definitionsScopedSearch, directClassInheritorsSearch and methodReferencesSearch - to integrate with various sorts of search within IntelliJ.


For examples of all these items, I'd recommend looking at the Kotlin code, which uses light classes extensively.

Cheers,
Colin

0

Hi Colin,

Thanks for your answer. I will definitely be looking into it and will let you know here how it went.

I had a look at Kotlin PsiElementFinder before (one of the rare implementations I found on GitHub) but what I read didn't help me much.

Your pointers on the various actors in virtual classes handling might just make a big difference though.

Seb'

0

Hi Colin,

Thanks again for you answer earlier, the PsiShortNamesCache was what I needed to answer the specific question of this thread.

I will have to look into the other classes you suggested to implement a fully functional plugin though.

At the moment, I'm quite confused about the lifecycle of the PsiShortNameCache.
Every implementation I found is backed by StubIndex at some point (Kotlin plugin as well) and I don't see how I could use one in my specific case. I'm not implementing a custom language, just generating Java classes not backed by a file on disk, so I think IDEA's regular index are naturally used already. What puzzles me most is that I couldn't find any information about when information is removed from indexes or more generally about the lifecycle of indexes.

Anyway, I will be looking into that some more and will create a separate thread when I can be more accurate about it.

Thanks again,

Seb'

0

Please sign in to leave a comment.