@Override
  @NotNull
  public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    final PsiFile file = holder.getFile();
    if (!PsiUtil.isAvailable(JavaFeature.ANNOTATIONS, file) || nullabilityAnnotationsNotAvailable(file)) {
      return PsiElementVisitor.EMPTY_VISITOR;
    }
    return new JavaElementVisitor() {
      private final NullableNotNullManager manager = NullableNotNullManager.getInstance(holder.getProject());
      private final List<String> nullables = manager.getNullables();
      private final List<String> notNulls = manager.getNotNulls();

      @Override
      public void visitMethod(@NotNull PsiMethod method) {
        checkNullableStuffForMethod(method, holder);
      }

      @Override
      public void visitClass(@NotNull PsiClass aClass) {
        if (aClass.isRecord()) {
          PsiMethod constructor = JavaPsiRecordUtil.findCanonicalConstructor(aClass);
          if (constructor instanceof SyntheticElement) {
            checkParameters(constructor, holder, List.of(), manager);
          }
        }
      }

      @Override
      public void visitMethodReferenceExpression(@NotNull PsiMethodReferenceExpression expression) {
        checkMethodReference(expression, holder);

        JavaResolveResult result = expression.advancedResolve(false);
        PsiElement target = result.getElement();
        if (target instanceof PsiMethod) {
          checkCollectionNullityOnAssignment(expression,
                                             LambdaUtil.getFunctionalInterfaceReturnType(expression),
                                             result.getSubstitutor().substitute(((PsiMethod)target).getReturnType()));
        }
      }

      @Override
      public void visitField(@NotNull PsiField field) {
        final PsiType type = field.getType();
        final Annotated annotated = check(field, holder, type);
        if (TypeConversionUtil.isPrimitiveAndNotNull(type)) {
          return;
        }
        Project project = holder.getProject();
        if (annotated.isDeclaredNotNull ^ annotated.isDeclaredNullable) {
          final String anno = annotated.isDeclaredNotNull ? manager.getDefaultNotNull() : manager.getDefaultNullable();
          final List<String> annoToRemove = annotated.isDeclaredNotNull ? nullables : notNulls;

          if (!checkNonStandardAnnotations(field, annotated, anno, holder)) return;

          checkAccessors(field, annotated, project, manager, anno, annoToRemove, holder);

          checkConstructorParameters(field, annotated, manager, anno, annoToRemove, holder);
        }
      }

      @Override
      public void visitParameter(@NotNull PsiParameter parameter) {
        check(parameter, holder, parameter.getType());
      }

      @Override
      public void visitAnnotation(@NotNull PsiAnnotation annotation) {
        String qualifiedName = annotation.getQualifiedName();
        Optional<Nullability> nullabilityOptional = manager.getAnnotationNullability(qualifiedName);
        if (nullabilityOptional.isEmpty()) return;
        Nullability nullability = nullabilityOptional.get();
        PsiType type = AnnotationUtil.getRelatedType(annotation);
        PsiAnnotationOwner owner = annotation.getOwner();
        PsiModifierListOwner listOwner = owner instanceof PsiModifierList
                                         ? tryCast(((PsiModifierList)owner).getParent(), PsiModifierListOwner.class) : null;
        if (type instanceof PsiPrimitiveType) {
          PsiType targetType = listOwner instanceof PsiMethod
                               ? ((PsiMethod)listOwner).getReturnType()
                               : listOwner instanceof PsiVariable ? ((PsiVariable)listOwner).getType() : null;
          LocalQuickFix additionalFix = null;
          if (targetType instanceof PsiArrayType && targetType.getAnnotations().length == 0) {
            additionalFix = new MoveAnnotationToArrayFix();
          }
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.primitive.type.annotation", LocalQuickFix.notNullElements(additionalFix));
        }
        if (type instanceof PsiClassType) {
          PsiElement context = ((PsiClassType)type).getPsiContext();
          // outer type/package
          if (context instanceof PsiJavaCodeReferenceElement outerCtx) {
            PsiElement parent = context.getParent();
            if (parent instanceof PsiJavaCodeReferenceElement) {
              if (outerCtx.resolve() instanceof PsiPackage) {
                ModCommandAction action = new MoveAnnotationOnStaticMemberQualifyingTypeFix(annotation);
                reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.applied.to.package",
                                        LocalQuickFix.from(action));
              }
              else {
                ModCommandAction action = new MoveAnnotationOnStaticMemberQualifyingTypeFix(annotation);
                reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.outer.type",
                                        LocalQuickFix.from(action));
              }
            }
            if (parent instanceof PsiReferenceList) {
              PsiElement firstChild = parent.getFirstChild();
              if ((PsiUtil.isJavaToken(firstChild, JavaTokenType.EXTENDS_KEYWORD) ||
                   PsiUtil.isJavaToken(firstChild, JavaTokenType.IMPLEMENTS_KEYWORD)) &&
                  !(parent.getParent() instanceof PsiTypeParameter)) {
                reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.reference.list");
              }
            }
          }
        }
        if (listOwner instanceof PsiMethod && ((PsiMethod)listOwner).isConstructor()) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.constructor");
        }
        if (listOwner instanceof PsiEnumConstant) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.enum.constant");
        }
        if ((listOwner instanceof PsiLocalVariable ||
             listOwner instanceof PsiParameter && ((PsiParameter)listOwner).getDeclarationScope() instanceof PsiCatchSection)
            && !manager.canAnnotateLocals(qualifiedName)) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.local.variable");
        }
        if (type instanceof PsiWildcardType && manager.isTypeUseAnnotationLocationRestricted(qualifiedName)) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.wildcard");
        }
        if (owner instanceof PsiTypeParameter && manager.isTypeUseAnnotationLocationRestricted(qualifiedName)) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.type.parameter");
        }
        if (listOwner instanceof PsiReceiverParameter && nullability != Nullability.NOT_NULL) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.receiver.annotation");
        }
        checkOppositeAnnotationConflict(annotation, nullability);
        if (AnnotationUtil.NOT_NULL.equals(qualifiedName)) {
          PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue("exception");
          if (value instanceof PsiClassObjectAccessExpression) {
            PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(((PsiClassObjectAccessExpression)value).getOperand().getType());
            if (psiClass != null && !hasStringConstructor(psiClass)) {
              reportProblem(holder, value, "custom.exception.class.should.have.a.constructor");
            }
          }
        }
      }

      private void checkOppositeAnnotationConflict(PsiAnnotation annotation, Nullability nullability) {
        PsiAnnotationOwner owner = annotation.getOwner();
        if (owner == null) return;
        PsiModifierListOwner listOwner = owner instanceof PsiModifierList modifierList
                                         ? tryCast(modifierList.getParent(), PsiModifierListOwner.class)
                                         : null;
        Condition<PsiAnnotation> filter = anno ->
          anno != annotation && manager.getAnnotationNullability(anno.getQualifiedName()).filter(n -> n != nullability).isPresent();
        PsiAnnotation oppositeAnno = ContainerUtil.find(owner.getAnnotations(), filter);
        if (oppositeAnno == null && listOwner != null) {
          oppositeAnno = manager.findExplicitNullabilityAnnotation(
            listOwner, ContainerUtil.filter(Nullability.values(), n -> n != nullability));
        }
        if (oppositeAnno != null &&
            Objects.equals(AnnotationUtil.getRelatedType(annotation), AnnotationUtil.getRelatedType(oppositeAnno))) {
          reportProblem(holder, annotation, new RemoveAnnotationQuickFix(annotation, listOwner),
                        "inspection.nullable.problems.Nullable.NotNull.conflict",
                        getPresentableAnnoName(annotation), getPresentableAnnoName(oppositeAnno));
        }
      }

      private static boolean hasStringConstructor(PsiClass aClass) {
        for (PsiMethod method : aClass.getConstructors()) {
          PsiParameterList list = method.getParameterList();
          if (list.getParametersCount() == 1 &&
              Objects.requireNonNull(list.getParameter(0)).getType().equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
            return true;
          }
        }
        return false;
      }

      @Override
      public void visitReferenceElement(@NotNull PsiJavaCodeReferenceElement reference) {
        super.visitReferenceElement(reference);

        checkNullableNotNullInstantiationConflict(reference);

        PsiElement list = reference.getParent();
        PsiElement psiClass = list instanceof PsiReferenceList ? list.getParent() : null;
        PsiElement intf = reference.resolve();
        if (psiClass instanceof PsiClass && list == ((PsiClass)psiClass).getImplementsList() &&
            intf instanceof PsiClass && ((PsiClass)intf).isInterface()) {
          String error = checkIndirectInheritance(psiClass, (PsiClass)intf);
          if (error != null) {
            holder.registerProblem(reference, error);
          }
        }
      }

      private void checkNullableNotNullInstantiationConflict(PsiJavaCodeReferenceElement reference) {
        PsiElement element = reference.resolve();
        if (element instanceof PsiClass) {
          PsiTypeParameter[] typeParameters = ((PsiClass)element).getTypeParameters();
          PsiTypeElement[] typeArguments = getReferenceTypeArguments(reference);
          if (typeParameters.length > 0 && typeParameters.length == typeArguments.length && !(typeArguments[0].getType() instanceof PsiDiamondType)) {
            for (int i = 0; i < typeParameters.length; i++) {
              PsiTypeElement typeArgument = typeArguments[i];
              Project project = element.getProject();
              PsiType type = typeArgument.getType();
              if (DfaPsiUtil.getTypeNullability(JavaPsiFacade.getElementFactory(project).createType(typeParameters[i])) ==
                  Nullability.NOT_NULL) {
                Nullability typeNullability = DfaPsiUtil.getTypeNullability(type);
                if (typeNullability != Nullability.NOT_NULL &&
                    !(typeNullability == Nullability.UNKNOWN && type instanceof PsiWildcardType && !((PsiWildcardType)type).isExtends())) {
                  String annotationToAdd = manager.getDefaultNotNull();
                  PsiClass annotationClass = JavaPsiFacade.getInstance(project).findClass(annotationToAdd, element.getResolveScope());
                  AddTypeAnnotationFix fix = null;
                  if (annotationClass != null &&
                      AnnotationTargetUtil.findAnnotationTarget(annotationClass, PsiAnnotation.TargetType.TYPE_USE) != null) {
                    fix = new AddTypeAnnotationFix(annotationToAdd, manager.getNullables());
                  }
                  reportProblem(holder, typeArgument, fix, "non.null.type.argument.is.expected");
                }
              }
            }
          }
        }
      }

      private static PsiTypeElement[] getReferenceTypeArguments(PsiJavaCodeReferenceElement reference) {
        PsiReferenceParameterList typeArgList = reference.getParameterList();
        return typeArgList == null ? PsiTypeElement.EMPTY_ARRAY : typeArgList.getTypeParameterElements();
      }

      @Override
      public void visitAssignmentExpression(@NotNull PsiAssignmentExpression expression) {
        checkCollectionNullityOnAssignment(expression.getOperationSign(), expression.getLExpression().getType(), expression.getRExpression());
      }

      @Override
      public void visitLocalVariable(@NotNull PsiLocalVariable variable) {
        PsiIdentifier identifier = variable.getNameIdentifier();
        if (identifier != null) {
          checkCollectionNullityOnAssignment(identifier, variable.getType(), variable.getInitializer());
        }
      }

      @Override
      public void visitReturnStatement(@NotNull PsiReturnStatement statement) {
        PsiExpression returnValue = statement.getReturnValue();
        if (returnValue == null) return;

        checkCollectionNullityOnAssignment(statement.getReturnValue(), PsiTypesUtil.getMethodReturnType(statement), returnValue);
      }

      @Override
      public void visitLambdaExpression(@NotNull PsiLambdaExpression lambda) {
        super.visitLambdaExpression(lambda);
        PsiElement body = lambda.getBody();
        if (body instanceof PsiExpression) {
          checkCollectionNullityOnAssignment(body, LambdaUtil.getFunctionalInterfaceReturnType(lambda), (PsiExpression)body);
        }
      }

      @Override
      public void visitCallExpression(@NotNull PsiCallExpression callExpression) {
        PsiExpressionList argList = callExpression.getArgumentList();
        JavaResolveResult result = callExpression.resolveMethodGenerics();
        PsiMethod method = (PsiMethod)result.getElement();
        if (method == null || argList == null) return;

        PsiSubstitutor substitutor = result.getSubstitutor();
        PsiParameter[] parameters = method.getParameterList().getParameters();
        PsiExpression[] arguments = argList.getExpressions();
        for (int i = 0; i < arguments.length; i++) {
          PsiExpression argument = arguments[i];
          if (i < parameters.length &&
              (i < parameters.length - 1 || !MethodCallInstruction.isVarArgCall(method, substitutor, arguments, parameters))) {
            checkCollectionNullityOnAssignment(argument, substitutor.substitute(parameters[i].getType()), argument);
          }
        }
      }

      private void checkCollectionNullityOnAssignment(@NotNull PsiElement errorElement,
                                                      @Nullable PsiType expectedType,
                                                      @Nullable PsiExpression assignedExpression) {
        if (assignedExpression == null) return;

        checkCollectionNullityOnAssignment(errorElement, expectedType, assignedExpression.getType());
      }

      private void checkCollectionNullityOnAssignment(@NotNull PsiElement errorElement,
                                                      @Nullable PsiType expectedType,
                                                      @Nullable PsiType assignedType) {
        if (isNullableNotNullCollectionConflict(expectedType, assignedType, file, new HashSet<>())) {
          reportProblem(holder, errorElement, "assigning.a.collection.of.nullable.elements");
        }
      }
    };
  }
@Override
  @NotNull
  public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly)