@NotNull
  private static Iterable<@NotNull PsiClass> getOrComputeSubClasses(@NotNull Project project,
                                                                    @NotNull PsiClass baseClass,
                                                                    @NotNull SearchScope searchScopeForNonPhysical,
                                                                    @NotNull ClassInheritorsSearch.SearchParameters parameters) {
    HighlightingCaches caches = HighlightingCaches.getInstance(project);
    ConcurrentMap<PsiClass, Iterable<PsiClass>> map = parameters.isIncludeAnonymous()
                                                      ? caches.ALL_SUB_CLASSES
                                                      : caches.ALL_SUB_CLASSES_NO_ANONYMOUS;
    Iterable<PsiClass> cached = map.get(baseClass);
    if (cached == null) {
      // returns lazy collection of subclasses. Each call to next() leads to calculation of next batch of subclasses.
      Function<@NotNull PsiAnchor, @NotNull PsiClass> converter =
        anchor -> ReadAction.compute(() -> (@NotNull PsiClass)anchor.retrieve());
      Predicate<PsiClass> applicableFilter =
        candidate -> !(candidate instanceof PsiAnonymousClass) && candidate != null && !candidate.hasModifierProperty(PsiModifier.FINAL);
      // for non-physical elements ignore the cache completely because non-physical elements created so often/unpredictably so I can't figure out when to clear caches in this case
      boolean isPhysical = ReadAction.compute(baseClass::isPhysical);
      SearchScope scopeToUse = isPhysical ? GlobalSearchScope.allScope(project) : searchScopeForNonPhysical;
      LazyConcurrentCollection.MoreElementsGenerator<PsiAnchor, PsiClass> generator = (candidate, processor) -> DirectClassInheritorsSearch
          .search(new DirectClassInheritorsSearch.SearchParameters(candidate, scopeToUse, parameters.isIncludeAnonymous(), true) {
            @Override
            public boolean shouldSearchInLanguage(@NotNull Language language) {
              return parameters.shouldSearchInLanguage(language);
            }

            @Override
            public ClassInheritorsSearch.SearchParameters getOriginalParameters() {
              return parameters;
            }
          })
          .allowParallelProcessing().forEach(subClass -> {
            ProgressManager.checkCanceled();
            @NotNull PsiAnchor pointer = ReadAction.compute(() -> PsiAnchor.create(subClass));
            // append found result to subClasses as early as possible to allow other waiting threads to continue
            processor.accept(pointer);
            return true;
          });

      PsiAnchor seed = ReadAction.compute(() -> PsiAnchor.create(baseClass));
      // lazy collection: store underlying queue as PsiAnchors, generate new elements by running direct inheritors
      Iterable<PsiClass> computed = new LazyConcurrentCollection<>(seed, converter, applicableFilter, generator);
      // make sure concurrent calls of this method always return the same collection to avoid expensive duplicate work
      cached = isPhysical ? ConcurrencyUtil.cacheOrGet(map, baseClass, computed) : computed;
    }
    return cached;
  }
@NotNull
  private static Iterable<@NotNull PsiClass> getOrComputeSubClasses(@NotNull Project project,
                                                                    @NotNull PsiClass baseClass,
                                                                    @NotNull SearchScope searchScopeForNonPhysical,
                                                                    @NotNull ClassInheritorsSearch.SearchParameters parameters)