@Override
  public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
    return new JavaElementVisitor() {
      private final NotNullLazyValue<Map<String, PsiClass>> myGuavaClassConversions = NotNullLazyValue.atomicLazy(() -> {
        Map<String, PsiClass> map = new HashMap<>();
        for (TypeConversionRule rule : TypeConversionRule.EP_NAME.getExtensions()) {
          if (rule instanceof BaseGuavaTypeConversionRule) {
            final String fromClass = ((BaseGuavaTypeConversionRule)rule).ruleFromClass();
            final String toClass = ((BaseGuavaTypeConversionRule)rule).ruleToClass();

            final Project project = holder.getProject();
            final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project);
            final PsiClass targetClass = javaPsiFacade.findClass(toClass, GlobalSearchScope.allScope(project));

            if (targetClass != null) {
              map.put(fromClass, targetClass);
            }
          }
        }
        return map;
      });

      @Override
      public void visitVariable(@NotNull PsiVariable variable) {
        if (!checkVariables) return;
        final PsiIdentifier identifier = variable.getNameIdentifier();
        if (identifier == null) return;
        final PsiType type = variable.getType();
        PsiType targetType = getConversionClassType(type);
        if (targetType != null) {
          holder.registerProblem(identifier,
                                 TypeMigrationBundle.message("guava.functional.primitives.can.be.replaced.by.java.api.problem.description"),
                                 new MigrateGuavaTypeFix(variable, targetType));
        }
      }

      @Override
      public void visitMethod(@NotNull PsiMethod method) {
        super.visitMethod(method);
        if (!checkReturnTypes) return;
        final PsiType targetType = getConversionClassType(method.getReturnType());
        if (targetType != null) {
          final PsiTypeElement typeElement = method.getReturnTypeElement();
          if (typeElement != null) {
            holder.registerProblem(typeElement,
                                   TypeMigrationBundle.message("guava.functional.primitives.can.be.replaced.by.java.api.problem.description"),
                                   new MigrateGuavaTypeFix(method, targetType));
          }
        }
      }

      @Override
      public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
        checkFluentIterableGenerationMethod(expression);
        checkPredicatesUtilityMethod(expression);
      }

      private void checkPredicatesUtilityMethod(@NotNull PsiMethodCallExpression expression) {
        if (GuavaPredicateConversionRule.isPredicates(expression) && isPossibleToConvertPredicate(expression)) {
          final PsiClassType initialType = (PsiClassType)expression.getType();
          if (initialType == null) return;
          PsiClassType targetType = createTargetType(initialType);
          if (targetType == null) return;
          final PsiElement anchor = expression.getMethodExpression().getReferenceNameElement();
          assert anchor != null;
          holder.registerProblem(anchor,
                                 TypeMigrationBundle.message("guava.functional.primitives.can.be.replaced.by.java.api.problem.description"),
                                 new MigrateGuavaTypeFix(expression, targetType));
        }
      }

      private static boolean isPossibleToConvertPredicate(@NotNull PsiMethodCallExpression expression) {
        PsiMethodCallExpression enclosingMethodCallExpression =
          PsiTreeUtil.getParentOfType(expression, PsiMethodCallExpression.class);

        return enclosingMethodCallExpression == null ||
               isReceiverOfEnclosingCallAFluentIterable(enclosingMethodCallExpression) ||
               GuavaPredicateConversionRule.isEnclosingCallAPredicate(enclosingMethodCallExpression) ||
               GuavaPredicateConversionRule.isPredicateConvertibleInsideEnclosingMethod(expression, enclosingMethodCallExpression);
      }

      private void checkFluentIterableGenerationMethod(PsiMethodCallExpression expression) {
        if (!checkChains) return;
        if (!isFluentIterableFromCall(expression)) return;

        final PsiMethodCallExpression chain = findGuavaMethodChain(expression);
        if (chain == null) {
          return;
        }

        PsiClassType initialType = (PsiClassType)chain.getType();
        LOG.assertTrue(initialType != null);
        PsiClassType targetType = createTargetType(initialType);
        if (targetType == null) return;

        PsiElement highlightedElement = chain;
        if (chain.getParent() instanceof PsiReferenceExpression && chain.getParent().getParent() instanceof PsiMethodCallExpression) {
          highlightedElement = chain.getParent().getParent();
        }
        holder.registerProblem(highlightedElement,
                               TypeMigrationBundle.message("guava.functional.primitives.can.be.replaced.by.java.api.problem.description"), new MigrateGuavaTypeFix(chain, targetType));
      }

      @Nullable
      private PsiClassType createTargetType(PsiClassType initialType) {
        PsiClass resolvedClass = initialType.resolve();
        PsiClass target;
        if (resolvedClass == null || (target = myGuavaClassConversions.getValue().get(resolvedClass.getQualifiedName())) == null) {
          return null;
        }
        return addTypeParameters(initialType, initialType.resolveGenerics(), target);
      }

      private PsiType getConversionClassType(PsiType initialType) {
        if (initialType == null) return null;
        final PsiType type = initialType.getDeepComponentType();
        if (type instanceof PsiClassType) {
          final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)type).resolveGenerics();
          final PsiClass psiClass = resolveResult.getElement();
          if (psiClass != null) {
            final String qName = psiClass.getQualifiedName();
            final PsiClass targetClass = myGuavaClassConversions.getValue().get(qName);
            if (targetClass != null) {
              final PsiClassType createdType = addTypeParameters(type, resolveResult, targetClass);
              return initialType instanceof PsiArrayType ? wrapAsArray((PsiArrayType)initialType, createdType) : createdType;
            }
          }
        }
        return null;
      }

      private static PsiType wrapAsArray(PsiArrayType initial, PsiType created) {
        PsiArrayType result = new PsiArrayType(created);
        while (initial.getComponentType() instanceof PsiArrayType) {
          initial = (PsiArrayType)initial.getComponentType();
          result = new PsiArrayType(result);
        }
        return result;
      }

      public static boolean isReceiverOfEnclosingCallAFluentIterable(@NotNull PsiMethodCallExpression enclosingMethodCallExpression) {
        PsiMethod method = enclosingMethodCallExpression.resolveMethod();
        if (method == null) return false;
        return isFluentIterable(method.getContainingClass());
      }

      private static boolean isFluentIterableFromCall(PsiMethodCallExpression expression) {
        PsiMethod method = expression.resolveMethod();
        if (method == null || !GuavaFluentIterableConversionRule.CHAIN_HEAD_METHODS.contains(method.getName())) {
          return false;
        }
        PsiClass aClass = method.getContainingClass();
        return isFluentIterable(aClass);
      }

      private static boolean isFluentIterable(PsiClass aClass) {
        return aClass != null && (GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(aClass.getQualifiedName()) ||
                                  GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(aClass.getQualifiedName()));
      }

      private static PsiMethodCallExpression findGuavaMethodChain(PsiMethodCallExpression expression) {
        PsiMethodCallExpression chain = expression;
        while (true) {
          final PsiMethodCallExpression current = PsiTreeUtil.getParentOfType(chain, PsiMethodCallExpression.class);
          if (current != null && current.getMethodExpression().getQualifierExpression() == chain) {
            final PsiMethod method = current.resolveMethod();
            if (method == null) {
              return chain;
            }
            if (FLUENT_ITERABLE_STOP_METHODS.contains(method.getName())) {
              return null;
            }
            final PsiClass containingClass = method.getContainingClass();
            if (containingClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(containingClass.getQualifiedName())
                                             || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(containingClass.getQualifiedName()))) {
              return chain;
            }
            final PsiType returnType = method.getReturnType();
            final PsiClass returnClass = PsiTypesUtil.getPsiClass(returnType);
            if (returnClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(returnClass.getQualifiedName())
                                    || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(returnClass.getQualifiedName()))) {
              return chain;
            }
            if (GuavaTypeConversionDescriptor.isIterable(current)) {
              return chain;
            }
          }
          else {
            return chain;
          }
          chain = current;
        }
      }

      @NotNull
      private PsiClassType addTypeParameters(PsiType currentType,
                                             PsiClassType.ClassResolveResult currentTypeResolveResult,
                                             PsiClass targetClass) {
        final Map<PsiTypeParameter, PsiType> substitutionMap = currentTypeResolveResult.getSubstitutor().getSubstitutionMap();
        final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(holder.getProject());
        if (substitutionMap.size() == 1) {
          return elementFactory.createType(targetClass, ContainerUtil.getFirstItem(substitutionMap.values()));
        }
        else {
          LOG.assertTrue(substitutionMap.size() == 2);
          LOG.assertTrue(GuavaLambda.FUNCTION.getJavaAnalogueClassQName().equals(targetClass.getQualifiedName()));
          final PsiType returnType = LambdaUtil.getFunctionalInterfaceReturnType(currentType);
          final List<PsiType> types = new ArrayList<>(substitutionMap.values());
          types.remove(returnType);
          final PsiType parameterType = types.get(0);
          return elementFactory.createType(targetClass, parameterType, returnType);
        }
      }
    };
  }
@Override
  public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly)