// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.nullable;

import com.intellij.codeInsight.*;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.MoveAnnotationOnStaticMemberQualifyingTypeFix;
import com.intellij.codeInsight.intention.AddAnnotationModCommandAction;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.codeInsight.intention.AddTypeAnnotationFix;
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
import com.intellij.codeInspection.dataFlow.java.inst.MethodCallInstruction;
import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.openapi.command.undo.UndoUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.GeneratedSourcesFilter;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.impl.search.JavaNullMethodArgumentUtil;
import com.intellij.psi.impl.search.JavaOverridingMethodsSearcher;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
import com.intellij.psi.util.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.JavaTypeNullabilityUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.TypeUtils;
import one.util.streamex.StreamEx;
import org.jdom.Element;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;

import java.util.*;
import java.util.function.Consumer;

import static com.intellij.codeInsight.AnnotationUtil.*;
import static com.intellij.codeInspection.nullable.AnnotateAsNullMarkedFix.createAnnotateAsNullMarkedFix;
import static com.intellij.patterns.PsiJavaPatterns.psiElement;
import static com.intellij.patterns.PsiJavaPatterns.psiMethod;
import static com.intellij.util.ObjectUtils.tryCast;
import static java.util.Objects.requireNonNull;

public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspectionTool {
  /**
   * @deprecated the field remains to minimize changes to users' inspection profiles.
   */
  @Deprecated @SuppressWarnings("WeakerAccess") public boolean REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL = true;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL = true;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE = true;
  /**
   * @deprecated the field remains to minimize changes to users' inspection profiles.
   */
  @Deprecated @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL = true;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_ANNOTATED_GETTER = true;
  @SuppressWarnings("WeakerAccess") public boolean IGNORE_EXTERNAL_SUPER_NOTNULL;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOTNULL_PARAMETERS_OVERRIDES_NOT_ANNOTATED;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NULLABILITY_ANNOTATION_ON_LOCALS = true;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_ANNOTATED_INSTANTIATION_NOT_NULL_TYPE = false;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = false;
  /**
   * @deprecated the field remains to minimize changes to users' inspection profiles.
   */
  @Deprecated @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_ANNOTATED_SETTER_PARAMETER = true;
  /**
   * @deprecated field remains for test
   */
  @Deprecated @SuppressWarnings("WeakerAccess") public boolean REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS = true;
  /**
   * @deprecated the field remains to minimize changes to users' inspection profiles.
   */
  @Deprecated @SuppressWarnings("WeakerAccess") public boolean REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD = true;
  public boolean REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER = true;
  @SuppressWarnings("WeakerAccess") public boolean REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER = true;

  private static final Logger LOG = Logger.getInstance(NullableStuffInspectionBase.class);

  @Override
  public void writeSettings(@NotNull Element node) throws WriteExternalException {
    super.writeSettings(node);
    for (Element child : new ArrayList<>(node.getChildren())) {
      String name = child.getAttributeValue("name");
      String value = child.getAttributeValue("value");
      if ("IGNORE_EXTERNAL_SUPER_NOTNULL".equals(name) && "false".equals(value) ||
          "REPORT_NOTNULL_PARAMETERS_OVERRIDES_NOT_ANNOTATED".equals(name) && "false".equals(value) ||
          "REQUIRE_NOTNULL_FIELDS_INITIALIZED".equals(name) && "true".equals(value) ||
          "REPORT_NULLABILITY_ANNOTATION_ON_LOCALS".equals(name) && "true".equals(value) ||
          "REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER".equals(name) && "true".equals(value) ||
          "REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS".equals(name) && "false".equals(value) ||
          "REPORT_NOT_ANNOTATED_INSTANTIATION_NOT_NULL_TYPE".equals(name) && "false".equals(value) ||
          "REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER".equals(name) && "true".equals(value)) {
        node.removeContent(child);
      }
    }
  }

  @Override
  public @NotNull PsiElementVisitor buildVisitor(final @NotNull 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);
          }
        }
        checkConflictingContainerAnnotations(holder, aClass.getModifierList());
      }

      @Override
      public void visitPackageStatement(@NotNull PsiPackageStatement statement) {
        checkConflictingContainerAnnotations(holder, statement.getAnnotationList());
      }

      @Override
      public void visitModule(@NotNull PsiJavaModule module) {
        checkConflictingContainerAnnotations(holder, module.getModifierList());
      }

      @Override
      public void visitNewExpression(@NotNull PsiNewExpression expression) {
        if (expression.isArrayCreation()) return;
        PsiJavaCodeReferenceElement ref = expression.getClassOrAnonymousClassReference();
        if (ref == null) return;
        if (!(ref.resolve() instanceof PsiClass cls)) return;
        String qualifiedName = cls.getQualifiedName();
        if (qualifiedName == null) return;
        PsiExpressionList list = expression.getArgumentList();
        if (list == null) return;
        if (!(expression.getType() instanceof PsiClassType type) || type.getParameterCount() != 1) return;
        PsiType typeParameter = type.getParameters()[0];
        if (!(typeParameter instanceof PsiClassType classType) || 
            DfaPsiUtil.getTypeNullability(typeParameter) != Nullability.NOT_NULL) return;
        boolean matched = switch (qualifiedName) {
          case "java.util.concurrent.atomic.AtomicReference" -> list.getExpressionCount() == 0;
          case "java.util.concurrent.atomic.AtomicReferenceArray" ->
            list.getExpressionCount() == 1 && PsiTypes.intType().equals(list.getExpressionTypes()[0]);
          case "java.lang.ThreadLocal" -> list.getExpressionCount() == 0 && expression.getAnonymousClass() == null;
          default -> false;
        };
        if (!matched) return;
        
        AddTypeAnnotationFix fix = null;
        if (classType.getPsiContext() instanceof PsiJavaCodeReferenceElement typeRef &&
            typeRef.getParent() instanceof PsiTypeElement typeElement && typeElement.getType().equals(classType) &&
            typeElement.acceptsAnnotations()) {
          fix = new AddTypeAnnotationFix(typeElement, manager.getDefaultAnnotation(Nullability.NULLABLE, expression), notNulls);
        }
        holder.problem(expression,
                       JavaAnalysisBundle.message("inspection.nullable.problems.constructor.not.compatible.non.null.type.argument"))
          .maybeFix(fix)
          .register();
      }

      @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 =
            manager.getDefaultAnnotation(annotated.isDeclaredNotNull ? Nullability.NOT_NULL : Nullability.NULLABLE, field);
          final List<String> annoToRemove = annotated.isDeclaredNotNull ? nullables : notNulls;

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

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

      @Override
      public void visitMethodCallExpression(@NotNull PsiMethodCallExpression call) {
        super.visitMethodCallExpression(call);
        PsiReferenceParameterList parameterList = call.getMethodExpression().getParameterList();
        if (parameterList == null) return;
        PsiType[] parameterization = parameterList.getTypeArguments();
        if (parameterization.length == 0) return;
        PsiMethod method = call.resolveMethod();
        if (method == null) return;
        PsiTypeParameter[] typeParameters = method.getTypeParameters();
        if (typeParameters.length != parameterization.length) return;
        for (int i = 0; i < typeParameters.length; i++) {
          PsiTypeParameter typeParameter = typeParameters[i];
          PsiType instance = parameterization[i];
          TypeNullability nullability = TypeNullability.ofTypeParameter(typeParameter);
          if (nullability.nullability() != Nullability.NOT_NULL) continue;
          TypeNullability instanceNullability = instance.getNullability();
          if (instanceNullability.nullability() == Nullability.NOT_NULL) continue;
          NullabilitySource source = instanceNullability.source();
          if (source instanceof NullabilitySource.ExplicitAnnotation explicit) {
            PsiAnnotation anchor = explicit.annotation();
            PsiJavaCodeReferenceElement ref = anchor.getNameReferenceElement();
            if (ref != null) {
              reportProblem(holder, anchor, "inspection.nullable.problems.nullable.instantiation.of.notnull",
                            typeParameter.getName(), ref.getReferenceName());
            }
          } else if (source instanceof NullabilitySource.ContainerAnnotation container) {
            PsiElement anchor = parameterList.getTypeParameterElements()[i];
            PsiJavaCodeReferenceElement ref = container.annotation().getNameReferenceElement();
            if (ref != null) {
              reportProblem(holder, anchor, "inspection.nullable.problems.nullable.instantiation.of.notnull.container",
                            typeParameter.getName(), ref.getReferenceName());
            }
          }
        }
      }

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

      @Override
      public void visitAnnotation(@NotNull PsiAnnotation annotation) {
        String qualifiedName = annotation.getQualifiedName();
        if (qualifiedName == null) return;
        Optional<Nullability> nullabilityOptional = manager.getAnnotationNullability(qualifiedName);
        if (nullabilityOptional.isEmpty()) return;
        Nullability nullability = nullabilityOptional.get();
        PsiType type = getRelatedType(annotation);
        PsiAnnotationOwner owner = annotation.getOwner();
        PsiModifierListOwner listOwner = owner instanceof PsiModifierList modifierList
                                         ? tryCast(modifierList.getParent(), PsiModifierListOwner.class) : null;
        PsiType targetType = listOwner == null ? null : PsiUtil.getTypeByPsiElement(listOwner);
        if (listOwner != null && targetType != null) {
          checkRedundantInContainerScope(annotation, manager.findContainerAnnotation(listOwner), nullability);
        }
        else if (type != null) {
          PsiElement context = type instanceof PsiClassType classType ? classType.getPsiContext() : annotation;
          if (context != null) {
            checkRedundantInContainerScope(annotation, manager.findDefaultTypeUseNullability(context), nullability);
          }
        }
        if (type != null && nullability == Nullability.NOT_NULL && PsiUtil.resolveClassInClassTypeOnly(type) instanceof PsiTypeParameter) {
          PsiType notAnnotated = type.annotate(TypeAnnotationProvider.EMPTY);
          TypeNullability notAnnotatedNullability = notAnnotated.getNullability();
          if (notAnnotatedNullability.nullability() == Nullability.NOT_NULL &&
              notAnnotatedNullability.source() instanceof NullabilitySource.ExtendsBound) {
            reportProblem(holder, annotation,
                          new RemoveAnnotationQuickFix(annotation, null),
                          "inspection.nullable.problems.redundant.annotation.inherited.notnull");
          }
        }
        if (type instanceof PsiPrimitiveType) {
          LocalQuickFix additionalFix = null;
          if (targetType instanceof PsiArrayType && !targetType.hasAnnotations()) {
            additionalFix = new MoveAnnotationToArrayFix();
          }
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.primitive.type.annotation", LocalQuickFix.notNullElements(additionalFix));
        }
        if (type instanceof PsiClassType classType) {
          PsiElement context = classType.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 (PsiUtil.isJavaToken(firstChild, JavaTokenType.THROWS_KEYWORD)) {
                reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.throws");
              }
            }
          }
        }
        if (type instanceof PsiArrayType && annotation.getParent() instanceof PsiTypeElement parent &&
            parent.getType().equals(type) && !manager.canAnnotateLocals(qualifiedName)) {
          checkIllegalLocalAnnotation(annotation, parent.getParent());
        }
        if (listOwner instanceof PsiMethod method && method.isConstructor()) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.constructor");
        }
        if (listOwner instanceof PsiClass && AnnotationTargetUtil.findAnnotationTarget(annotation, PsiAnnotation.TargetType.TYPE) == null) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.class");
        }
        if (listOwner instanceof PsiEnumConstant) {
          reportIncorrectLocation(holder, annotation, listOwner, "inspection.nullable.problems.at.enum.constant");
        }
        if (!manager.canAnnotateLocals(qualifiedName) && !(targetType instanceof PsiArrayType)) {
          checkIllegalLocalAnnotation(annotation, listOwner);
        }
        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 (NOT_NULL.equals(qualifiedName)) {
          PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue("exception");
          if (value instanceof PsiClassObjectAccessExpression classObjectAccessExpression) {
            PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(classObjectAccessExpression.getOperand().getType());
            if (psiClass != null && !hasStringConstructor(psiClass)) {
              reportProblem(holder, value, "custom.exception.class.should.have.a.constructor");
            }
          }
        }
      }

      private void checkRedundantInContainerScope(@NotNull PsiAnnotation annotation,
                                                  @Nullable NullabilityAnnotationInfo containerInfo,
                                                  @NotNull Nullability nullability) {
        if (!REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER) return;
        if (containerInfo != null && !containerInfo.getAnnotation().equals(annotation) && containerInfo.getNullability() == nullability) {
          PsiJavaCodeReferenceElement containerName = containerInfo.getAnnotation().getNameReferenceElement();
          if (containerName != null) {
            LocalQuickFix updateOptionFix = LocalQuickFix.from(
              new UpdateInspectionOptionFix(NullableStuffInspectionBase.this,
                                            "REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER",
                                            JavaAnalysisBundle.message("inspection.nullable.problems.turn.off.redundant.annotation.under.container"),
                                            false));
            reportProblem(holder, annotation,
                          LocalQuickFix.notNullElements(new RemoveAnnotationQuickFix(annotation, null), updateOptionFix),
                          "inspection.nullable.problems.redundant.annotation.under.container", containerName.getReferenceName());
          }
        }
      }

      private void checkIllegalLocalAnnotation(@NotNull PsiAnnotation annotation, @Nullable PsiElement owner) {
        if (!REPORT_NULLABILITY_ANNOTATION_ON_LOCALS) return;
        if (owner instanceof PsiLocalVariable ||
            owner instanceof PsiParameter parameter &&
            parameter.getDeclarationScope() instanceof PsiCatchSection) {
          reportIncorrectLocation(holder, annotation, (PsiVariable)owner, "inspection.nullable.problems.at.local.variable");
        }
      }

      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) {
          NullabilityAnnotationInfo result = manager.findNullabilityAnnotationInfo(
            listOwner, ContainerUtil.filter(Nullability.values(), n -> n != nullability));
          oppositeAnno = result == null || result.isContainer() ? null : result.getAnnotation();
        }
        if (oppositeAnno != null &&
            Objects.equals(getRelatedType(annotation), 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 &&
              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 (TypeNullability.ofTypeParameter(typeParameters[i]).nullability() != Nullability.NOT_NULL) continue;
              TypeNullability nullability = type.getNullability();
              Nullability typeNullability = nullability.nullability();
              if (typeNullability != Nullability.NOT_NULL &&
                  !(typeNullability == Nullability.UNKNOWN && type instanceof PsiWildcardType wildcardType && !wildcardType.isExtends())) {
                String annotationToAdd = manager.getDefaultAnnotation(Nullability.NOT_NULL, reference);
                PsiClass annotationClass = JavaPsiFacade.getInstance(project).findClass(annotationToAdd, element.getResolveScope());
                List<LocalQuickFix> fixes = new ArrayList<>();
                if (annotationClass != null &&
                    AnnotationTargetUtil.findAnnotationTarget(annotationClass, PsiAnnotation.TargetType.TYPE_USE) != null) {
                  fixes.add(LocalQuickFix.from(new AddTypeAnnotationFix(typeArgument, annotationToAdd, manager.getNullables())));
                }
                fixes.add(LocalQuickFix.from(createAnnotateAsNullMarkedFix(typeArgument, manager.getNullables()), false));
                ProblemHighlightType level =
                  nullability == TypeNullability.UNKNOWN && !REPORT_NOT_ANNOTATED_INSTANTIATION_NOT_NULL_TYPE ?
                  ProblemHighlightType.INFORMATION :
                  ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
                if (!isOnTheFly && level == ProblemHighlightType.INFORMATION) continue;
                holder.registerProblem(typeArgument,
                                       JavaAnalysisBundle.message("non.null.type.argument.is.expected"),
                                       level,
                                       fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
              }
            }
          }
        }
      }

      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;

        checkGenericClassOnReturn(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 checkGenericClassOnReturn(@Nullable PsiType expectedType,
                                             @NotNull PsiExpression returnValue) {
        PsiType returnType = returnValue.getType();
        JavaTypeNullabilityUtil.NullabilityConflictContext
          context = JavaTypeNullabilityUtil.getNullabilityConflictInAssignment(expectedType, returnType,
                                                                               REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS);
        if (context.nullabilityConflict() == JavaTypeNullabilityUtil.NullabilityConflict.UNKNOWN) return;
        String messageKey = context.nullabilityConflict() == JavaTypeNullabilityUtil.NullabilityConflict.NOT_NULL_TO_NULL ?
                            "returning.a.class.with.notnull.arguments" : "returning.a.class.with.nullable.arguments";

        reportProblem(holder, returnValue, LocalQuickFix.EMPTY_ARRAY,
                      messageKey, new Object[]{""},
                      messageKey, new Object[]{NullableStuffInspectionUtil.getNullabilityConflictPresentation(context)});
      }

      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");
        }
      }
    };
  }

  private void checkConflictingContainerAnnotations(@NotNull ProblemsHolder holder, @Nullable PsiModifierList list) {
    if (list == null || !list.hasAnnotations()) return;
    NullableNotNullManager manager = NullableNotNullManager.getInstance(holder.getProject());
    List<PsiAnnotation> conflictingAnnotations = manager.getConflictingContainerAnnotations(list);
    if (!conflictingAnnotations.isEmpty()) {
      for (PsiAnnotation annotation : conflictingAnnotations) {
        reportProblem(holder, annotation, "conflicting.nullability.annotations");
      }
    }
  }

  private void reportProblem(@NotNull ProblemsHolder holder, PsiElement anchor, String messageKey, Object... args) {
    reportProblem(holder, anchor, LocalQuickFix.EMPTY_ARRAY, messageKey, args);
