JavaPsiFacade.findClass() starts returning null after rootsChanged event

hello there,
I've encountered a strange problem trying to refresh my plugin's internal data structures after 'rootsChanged' event : JavaPsiFacade.findClass() suddenly starts returning null  for previously tested class names (i.e. names of classes located properly using the same findClass() method on project startup). Here's the source code of my rootsChanged callback method :

// project source root / library class root added / removed -> rebuild structures
        myProject.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
            @Override
            public void rootsChanged(ModuleRootEvent event) {
                LOG.debug("-- rootsChanged event fired -> rebuilding data structures");
                try {

                    ProjectRootManager rootManager = ProjectRootManager.getInstance(myProject);
                    VirtualFile[] sourceRoots = rootManager.getContentSourceRoots();
                    VirtualFile[] libraryClassRoots = rootManager.orderEntries().librariesOnly().getClassesRoots();

                    HashSet<VirtualFile> currentContentRoots = new HashSet<>();
                    ContainerUtil.addAll(currentContentRoots, sourceRoots);
                    ContainerUtil.addAll(currentContentRoots, libraryClassRoots);

                    HashSet<VirtualFile> unmatchedContentRoots = new HashSet<>();
                    // copy to prevent concurrent modification exception..
                    Sets.symmetricDifference(knownContentRoots, currentContentRoots).copyInto(unmatchedContentRoots);

                    // scan project content roots for changes (new / removed source roots, new / removed library class roots)

                    LOG.debug("\nunmatched project content roots :");
                    for (VirtualFile unmatchedRoot : unmatchedContentRoots) {
                        String info = "content root [" + unmatchedRoot.getPresentableUrl() + "] : {}";
                        if (!knownContentRoots.contains(unmatchedRoot)) {
                            LOG.debug(info, "new -> scanning for structure definition files");
                            // new content root -> scan for structure definition files..
                            knownContentRoots.add(unmatchedRoot);
                            rebuildStructures(unmatchedRoot);
                            continue;
                        }
                        // deleted content root -> update related managers and delete root from knownContentRoots collection
                        LOG.debug(info, "removed from project structure");
                        removeDataObjects(unmatchedRoot);
                        knownContentRoots.remove(unmatchedRoot);
                    }

                } catch (RuntimeException re) {
                    throw ExceptionDecorator.processException(re, "Error updating data structures after rootsChanged event"
                         , myProject, DumbService.isDumb(myProject));
                }
            }
        });


The magic begins with 'rebuildStructures(unmatchedRoot)' method call -> that's where I'm trying to iterate over the contents of unmatchedRoot, searching for interesting input files for my data structure builders. To classify a file as 'interesting' I check the inheritance chains of contained classes and if a given PsiClass descends from a specific superclass - I parse its' contents and initialize my custom data holders.
I'm using InheritanceUtils.isInheritor(PsiClass psiClass, String baseClassName) method to check whether a PsiClass is a descendant of a specific superclass - and that's where the problem occurs. I stepped into InheritanceUtil.isInheritor() method using IDEA's debugger and this code snippet seems to be causing trouble :

       final PsiClass base = JavaPsiFacade.getInstance(psiClass.getProject()).findClass(baseClassName, psiClass.getResolveScope());
       if (base == null) {
          return false;
       }


'base' == null since findClass() cannot locate the superclass in question. I checked psiClass.getResolveScope() and it seems ok (!= null, contains everything excluding test-scoped dependencies, no exceptions are thrown..)
I must add that the base class is often a library-scoped dependency (i.e. it's a class file in one of the dependent jars from the local maven repository) but on project startup it doesn't seem to pose any problem - only in rootsChanged these 'code pointers' are somehow forgotten.
What could be the reason of such behaviour ?

Thanks in advance for any assistance,
regards
Simon

8 comments
Comment actions Permalink

It seems that in rootsChanged callback method content roots are in some kind of 'inconsistent state' and JavaPsiFacade.findClass() may run into trouble trying to locate a class from there.
My sysouts suggest that the 'base' content root (jar file dependency, containing all the superclasses from which my 'interesting' input files descend) is reachable, the class file in question can be located (VirtualFileManager.getInstance().findFileByUrl("jar://d://.m2/repository/pl/com/tiger/luwak-base/1.0.5/luwak-base-1.0.5.jar!/pl/com/tiger/navigators/State.class") returns the State.class file) but 'pl.com.tiger.navigators.State' cannot be resolved using JavaPsiFacade's methods
Any thoughts on this one ?

0
Comment actions Permalink

I added

FileContentUtil.reparseFiles(myProject, currentContentRoots, true);


to my rootsChanged callback method and it makes referenced classes visible but also seems to trigger some additional events and throws exceptions so I'm sure there must be a better way to do it.

0
Comment actions Permalink

You're right, everything might be a bit inconsistent inside event handlers. The usual approach to such issues is to make the event handlers just do something very simple, like invalidating caches and/or scheduling an update later (e.g. using MergingUpdateQueue or Alarm). In that later update, the structures should be in a consistent state, so it's safe to query various indices. But please be aware that this might lock Swing thread and make IDEA unresponsive. Even if you're doing the update in background, don't take read action for too long.

0
Comment actions Permalink

thank you for your response, Peter
could you please shed some light on what kind of inconsistent state IDEA is in ? At first I thought that the 'dumb mode' is the one to blame for the exceptions I keep getting - but apparently it's not. Usually when some background tasks are involved there is some kind of semaphore showing whether the task is still under way. Or you can register a callback which gets invoked after the task has finished its' chores. Is there something like that in IDEA too ? You want me to just postpone my structure rebuild but how long should I wait ? Or maybe I can just use ApplicationManager.getApplication.invokeLater() to avoid the overhead imposed by MergingUpdateQueue (I tested it and it seems to work) ?
Also - MergingUpdateQueue and Alarm seem to be time-driven (they expect some time constraints as object constructor parameters) while one could expect something triggered by events.
I know I'm asking much and putting your kindness to the test but I'd really like to know HOW THINGS WORK, not just HOW TO MAKE THEM WORK :)

0
Comment actions Permalink

How things work is a tricky question, because there are quite a few those things, they all work slightly differently and all interoperate. In particular, if you do something in an event handler, bear in mind that other functionality also depends on those events, and their listeners might be called only after your, so they report the old state. rootsChanged also may trigger dumb mode when indices are not ready, but apparently it's not your case, or you'd get an exception.

It's a bad idea to do complex things in event handlers in general, because this slows down the event handling per se and makes IDEA unresponsive. So I'd recommend moving that functionality out even if it worked. :)

invokeLater could also work, although there might as well come several events of the same kind, so you'll end up with several invokeLaters doing the same work. That's why I recommend MergingUpdateQueue or Alarm. They don't have that much overhead. For those events, there's no kind of semaphore or listener that it's finished.

I don't know what your structure is so I can't recommend a definite solution. My favorite way in similar situations is to clear some cache in the event handler and then to calculate the structure when it's needed (hopefully it's not that expensive). And cache it again. CachedValue class helps in achieving this.

0
Comment actions Permalink

thank you for clarification and patience for newbish questions :)
One last thing - could you please post or point me to some example using MergingUpdateQueue ?

0
Comment actions Permalink

For example, com.intellij.codeInsight.completion.CompletionProgressIndicator#myQueue repaints the completion list every 300 ms when new items come in.

0
Comment actions Permalink

thank you once again Peter
That concludes my investigation :)

0

Please sign in to leave a comment.