// 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.dataFlow;

import com.intellij.codeInsight.*;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.codeInspection.dataFlow.interpreter.RunnerResult;
import com.intellij.codeInspection.dataFlow.interpreter.StandardDataFlowInterpreter;
import com.intellij.codeInspection.dataFlow.java.ControlFlowAnalyzer;
import com.intellij.codeInspection.dataFlow.java.JavaDfaListener;
import com.intellij.codeInspection.dataFlow.java.inst.MethodCallInstruction;
import com.intellij.codeInspection.dataFlow.jvm.descriptors.PlainDescriptor;
import com.intellij.codeInspection.dataFlow.lang.DfaListener;
import com.intellij.codeInspection.dataFlow.lang.ir.ControlFlow;
import com.intellij.codeInspection.dataFlow.lang.ir.DfaInstructionState;
import com.intellij.codeInspection.dataFlow.lang.ir.Instruction;
import com.intellij.codeInspection.dataFlow.lang.ir.ReturnInstruction;
import com.intellij.codeInspection.dataFlow.memory.DfaMemoryState;
import com.intellij.codeInspection.dataFlow.memory.DfaMemoryStateImpl;
import com.intellij.codeInspection.dataFlow.types.DfPrimitiveType;
import com.intellij.codeInspection.dataFlow.types.DfReferenceType;
import com.intellij.codeInspection.dataFlow.types.DfType;
import com.intellij.codeInspection.dataFlow.types.DfTypes;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.codeInspection.dataFlow.value.DfaVariableValue;
import com.intellij.codeInspection.dataFlow.value.RelationType;
import com.intellij.codeInspection.util.OptionalUtil;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.ControlFlowUtil;
import com.intellij.psi.impl.light.LightCompactConstructorParameter;
import com.intellij.psi.impl.light.LightRecordField;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.DeepestSuperMethodsSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.Stack;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.BoolUtils;
import com.siyeh.ig.psiutils.ControlFlowUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

import static com.intellij.psi.CommonClassNames.*;
import static com.intellij.util.ObjectUtils.tryCast;
import static com.siyeh.ig.callMatcher.CallMatcher.staticCall;

public final class DfaPsiUtil {
  private static final Logger LOG = Logger.getInstance(DfaPsiUtil.class);

  private static final CallMatcher NON_NULL_VAR_ARG = CallMatcher.anyOf(
    staticCall(JAVA_UTIL_LIST, "of"),
    staticCall(JAVA_UTIL_SET, "of"),
    staticCall(JAVA_UTIL_MAP, "ofEntries"));

  public static boolean isFinalField(PsiVariable var) {
    return var.hasModifierProperty(PsiModifier.FINAL) && !var.hasModifierProperty(PsiModifier.TRANSIENT) && var instanceof PsiField;
  }

  /**
   * Returns nullability of variable or method. This method takes into account various sources of nullability information,
   * like method annotations, type annotations, container annotations, inferred annotations, or external annotations.
   * 
   * @param resultType concrete type of particular variable access or method call (an instantiation of generic method return type, 
   *                   or variable type), if known.
   * @param owner method or variable to get its nullability
   * @return nullability of the owner; {@link Nullability#UNKNOWN} is both parameters are null.
   * @deprecated behaves like {@link #getElementNullabilityForWrite(PsiType, PsiModifierListOwner)}. 
   * Use either {@link #getElementNullabilityForWrite(PsiType, PsiModifierListOwner)} 
   * or {@link #getElementNullabilityForRead(PsiType, PsiModifierListOwner)} instead.
   */
  @Deprecated
  public static @NotNull Nullability getElementNullability(@Nullable PsiType resultType, @Nullable PsiModifierListOwner owner) {
    return getElementNullability(resultType, owner, false);
  }

  /**
   * Returns nullability of variable when it's expected to write to it, or nullability of method 
   * when it's expected to return a value from it.
   * For method parameter, it's expected that either the method is called (so the parameter is written by a call),
   * or the parameter is modified inside the method, like a local variable.
   * This method takes into account various sources of nullability information, 
   * like method annotations, type annotations, container annotations, inferred annotations, or external annotations.
   *
   * @param resultType concrete type of particular variable access or method call (an instantiation of generic method return type, 
   *                   or variable type), if known.
   * @param owner method or variable to get its nullability
   * @return nullability of the owner; {@link Nullability#UNKNOWN} is both parameters are null.
   */
  public static @NotNull Nullability getElementNullabilityForWrite(@Nullable PsiType resultType, @Nullable PsiModifierListOwner owner) {
    return getElementNullability(resultType, owner, false);
  }

  /**
   * Returns nullability of variable or method, when it's expected to read from it. 
   * This method takes into account various sources of nullability information, like method annotations, 
   * type annotations, container annotations, inferred annotations, or external annotations.
   * Automatic inference of method parameter nullability is ignored, which is useful when analyzing the method body (as it's
   * inferred from method body as well, so both analyses may produce conflicting results).
   *
   * @param resultType concrete type of particular variable access or method call (an instantiation of generic method return type, 
   *                   or variable type), if known.
   * @param owner method or variable to get its nullability
   * @return nullability of the owner; {@link Nullability#UNKNOWN} is both parameters are null.
   */
  public static @NotNull Nullability getElementNullabilityForRead(@Nullable PsiType resultType, @Nullable PsiModifierListOwner owner) {
    return getElementNullability(resultType, owner, true);
  }

  private static @NotNull Nullability getElementNullability(@Nullable PsiType resultType,
                                                            @Nullable PsiModifierListOwner owner,
                                                            boolean forRead) {
    if (owner == null) return getTypeNullability(resultType, forRead);

    if (resultType instanceof PsiPrimitiveType) {
      return Nullability.UNKNOWN;
    }

    if (owner instanceof PsiEnumConstant) {
      return Nullability.NOT_NULL;
    }

    // Annotation manager requires index
    Project project = owner.getProject();
    if (DumbService.isDumb(project)) return Nullability.UNKNOWN;
    NullabilityAnnotationInfo fromAnnotation = getNullabilityFromAnnotation(owner, forRead);
    if (fromAnnotation != null) {
      if (resultType != null && fromAnnotation.getNullability() != Nullability.NOT_NULL) {
        PsiType type = PsiUtil.getTypeByPsiElement(owner);
        if (type != null) {
          PsiAnnotationOwner annotationOwner = fromAnnotation.getAnnotation().getOwner();
          if (PsiUtil.resolveClassInClassTypeOnly(type) instanceof PsiTypeParameter tp &&
              annotationOwner instanceof PsiType && annotationOwner != type &&
              !tp.equals(PsiUtil.resolveClassInClassTypeOnly(resultType))) {
            // Nullable/Unknown from type hierarchy: should check the instantiation, as it could be more concrete
            return getTypeNullability(resultType, forRead);
          }
        }
      }
      return fromAnnotation.getNullability();
    }

    if (owner instanceof PsiMethod method) {
      if (isEnumPredefinedMethod(method)) {
        return Nullability.NOT_NULL;
      }
      if (isMapMethodWithUnknownNullity(method)) {
        return getTypeNullability(resultType) == Nullability.NULLABLE ? Nullability.NULLABLE : Nullability.UNKNOWN;
      }
    }

    if (owner instanceof PsiParameter parameter) {
      Nullability nullability = inferParameterNullability(parameter);
      if (nullability != Nullability.UNKNOWN) {
        return nullability;
      }
    }

    Nullability fromType = getNullabilityFromType(resultType, owner);
    if (fromType != null) return fromType;

    if (owner instanceof PsiMethod method && method.getParameterList().isEmpty() && forRead) {
      PsiField field = PropertyUtil.getFieldOfGetter(method);
      if (field != null && getElementNullabilityForRead(resultType, field) == Nullability.NULLABLE) {
        return Nullability.NULLABLE;
      }
    }

    return Nullability.UNKNOWN;
  }

  private static @Nullable Nullability getNullabilityFromType(@Nullable PsiType resultType, @NotNull PsiModifierListOwner owner) {
    if (resultType == null) return null;
    TypeNullability typeNullability = resultType.getNullability();
    if (typeNullability.equals(TypeNullability.UNKNOWN)) return null;
    Nullability fromType = typeNullability.nullability();
    if (fromType == Nullability.NOT_NULL && hasNullContract(owner)) {
      return Nullability.UNKNOWN;
    }
    return fromType;
  }

  private static boolean hasNullContract(@NotNull PsiModifierListOwner owner) {
    if (owner instanceof PsiParameter parameter && parameter.getDeclarationScope() instanceof PsiMethod method) {
      int index = method.getParameterList().getParameterIndex(parameter);
      List<StandardMethodContract> contracts = JavaMethodContractUtil.getMethodContracts(method);
      return ContainerUtil.exists(contracts,
                               c -> c.getParameterConstraint(index) == StandardMethodContract.ValueConstraint.NULL_VALUE);
    }
    return false;
  }

  private static @Nullable NullabilityAnnotationInfo getNullabilityFromAnnotation(@NotNull PsiModifierListOwner owner,
                                                                                  boolean ignoreParameterNullabilityInference) {
    NullableNotNullManager manager = NullableNotNullManager.getInstance(owner.getProject());
    NullabilityAnnotationInfo info = manager.findEffectiveNullabilityInfo(owner);
    if (info == null || shouldIgnoreAnnotation(info.getAnnotation())) {
      return null;
    }
    if (ignoreParameterNullabilityInference && owner instanceof PsiParameter && info.isInferred()) {
      List<PsiParameter> supers = AnnotationUtil.getSuperAnnotationOwners((PsiParameter)owner);
      return StreamEx.of(supers).map(param -> manager.findEffectiveNullabilityInfo(param))
        .findFirst(i -> i != null && i.getInheritedFrom() == null && i.getNullability() == Nullability.NULLABLE)
        .orElse(null);
    }
    return info;
  }

  private static boolean isMapMethodWithUnknownNullity(@NotNull PsiMethod method) {
    String name = method.getName();
    if (!"get".equals(name) && !"remove".equals(name)) return false;
    PsiMethod superMethod = DeepestSuperMethodsSearch.search(method).findFirst();
    return ("java.util.Map." + name).equals(PsiUtil.getMemberQualifiedName(superMethod != null ? superMethod : method));
  }

  public static @NotNull Nullability inferParameterNullability(@NotNull PsiParameter parameter) {
    PsiElement parent = parameter.getParent();
    if (parent instanceof PsiParameterList) {
      PsiElement gParent = parent.getParent();
      if (gParent instanceof PsiLambdaExpression) {
        return getFunctionalParameterNullability((PsiLambdaExpression)gParent, ((PsiParameterList)parent).getParameterIndex(parameter));
      } else if (gParent instanceof PsiMethod && OptionalUtil.OPTIONAL_OF_NULLABLE.methodMatches((PsiMethod)gParent)) {
        return Nullability.NULLABLE;
      }
    }
    if (parent instanceof PsiForeachStatement) {
      return getTypeNullability(inferLoopParameterTypeWithNullability((PsiForeachStatement)parent));
    }
    return Nullability.UNKNOWN;
  }

  private static @Nullable PsiType inferLoopParameterTypeWithNullability(PsiForeachStatement loop) {
    PsiExpression iteratedValue = PsiUtil.skipParenthesizedExprDown(loop.getIteratedValue());
    if (iteratedValue == null) return null;

    PsiType iteratedType = iteratedValue.getType();
    if (iteratedValue instanceof PsiReferenceExpression refExpr &&
        refExpr.resolve() instanceof PsiParameter parameter &&
        parameter.getParent() instanceof PsiForeachStatement targetLoop &&
        PsiTreeUtil.isAncestor(targetLoop, loop, true) &&
        !ControlFlowUtil.isReassigned(parameter, new HashMap<>())) {
      iteratedType = inferLoopParameterTypeWithNullability(targetLoop);
    }
    return JavaGenericsUtil.getCollectionItemType(iteratedType, iteratedValue.getResolveScope());
  }

  private static @NotNull Nullability getTypeNullability(@Nullable PsiType type, boolean forRead) {
    if (type == null) return Nullability.UNKNOWN;
    if (type instanceof PsiCapturedWildcardType captured) {
      if (!forRead) {
        TypeNullability nullability = captured.getLowerBound().getNullability();
        if (nullability.source() instanceof NullabilitySource.ExtendsBound) {
          PsiElement context = captured.getContext();
          NullableNotNullManager manager = NullableNotNullManager.getInstance(context.getProject());
          if (manager != null) {
            NullabilityAnnotationInfo defaultNullability = manager.findDefaultTypeUseNullability(context);
            if (defaultNullability != null && defaultNullability.getNullability() == Nullability.NOT_NULL) {
              return Nullability.NOT_NULL;
            }
          }
        }
        return nullability.nullability();
      }
    }
    return type.getNullability().nullability();
  }

  public static @NotNull Nullability getTypeNullability(@Nullable PsiType type) {
    if (type == null) return Nullability.UNKNOWN;
    return type.getNullability().nullability();
  }

  public static @Nullable NullabilityAnnotationInfo getTypeNullabilityInfo(@Nullable PsiType type) {
    if (type == null || type instanceof PsiPrimitiveType) return null;
    return type.getNullability().toNullabilityAnnotationInfo();
  }

  private static boolean shouldIgnoreAnnotation(PsiAnnotation annotation) {
    PsiClass containingClass = PsiUtil.getContainingClass(annotation);
    if (containingClass == null || !containingClass.isValid()) return false;
    String qualifiedName = containingClass.getQualifiedName();
    // We deliberately ignore nullability annotations on Guava functional interfaces to avoid noise warnings
    // See IDEA-170548 for details
    return "com.google.common.base.Predicate".equals(qualifiedName) || "com.google.common.base.Function".equals(qualifiedName);
  }

  /**
   * Returns the nullability of functional expression parameter
   *
   * @param function functional expression
   * @param index parameter index
   * @return nullability, defined by SAM parameter annotations or known otherwise
   */
  public static @NotNull Nullability getFunctionalParameterNullability(PsiFunctionalExpression function, int index) {
    PsiClassType type = tryCast(LambdaUtil.getFunctionalInterfaceType(function, true), PsiClassType.class);
    PsiMethod sam = LambdaUtil.getFunctionalInterfaceMethod(type);
    if (sam != null) {
      PsiParameter parameter = sam.getParameterList().getParameter(index);
      if (parameter != null) {
        PsiType parameterType = type.resolveGenerics().getSubstitutor().substitute(parameter.getType());
        if (parameterType instanceof PsiWildcardType wildcardType) {
          parameterType = wildcardType.getBound();
        }
        if (parameterType != null) {
          TypeNullability typeNullability = parameterType.getNullability();
          if (!typeNullability.equals(TypeNullability.UNKNOWN)) {
            return typeNullability.nullability();
          }
        }
        return getElementNullabilityForWrite(null, parameter);
      }
    }
    return Nullability.UNKNOWN;
  }

  private static boolean isEnumPredefinedMethod(PsiMethod method) {
    return CallMatcher.enumValueOf().methodMatches(method) || CallMatcher.enumValues().methodMatches(method);
  }

  public static boolean isInitializedNotNull(PsiField field) {
    PsiClass containingClass = field.getContainingClass();
    if (containingClass == null) return false;

    boolean staticField = field.hasModifierProperty(PsiModifier.STATIC);
    for (PsiClassInitializer initializer : containingClass.getInitializers()) {
      if (initializer.getLanguage().isKindOf(JavaLanguage.INSTANCE) &&
          initializer.hasModifierProperty(PsiModifier.STATIC) == staticField &&
          getBlockNotNullFields(initializer.getBody()).contains(field)) {
        return true;
      }
    }
    if (staticField) return false;

    PsiMethod[] constructors = containingClass.getConstructors();
    if (constructors.length == 0) return false;

    for (PsiMethod method : constructors) {
      if (!method.getLanguage().isKindOf(JavaLanguage.INSTANCE) ||
          !getBlockNotNullFields(method.getBody()).contains(field)) {
        return false;
      }
    }
    return true;
  }

  private static Set<PsiField> getBlockNotNullFields(PsiCodeBlock body) {
    if (body == null) return Collections.emptySet();

    return CachedValuesManager.getCachedValue(body, new CachedValueProvider<>() {
      @Override
      public @NotNull Result<Set<PsiField>> compute() {
        PsiClass containingClass = PsiTreeUtil.getContextOfType(body, PsiClass.class);
        LOG.assertTrue(containingClass != null);
        DfaValueFactory factory = new DfaValueFactory(body.getProject());
        ControlFlow flow = ControlFlowAnalyzer.buildFlow(body, factory, true);
        if (flow == null) {
          return Result.create(Set.of(), body, PsiModificationTracker.MODIFICATION_COUNT);
        }
        flow.keepVariables(
          desc ->
            desc instanceof PlainDescriptor plainDescriptor && plainDescriptor.getPsiElement() instanceof PsiField field &&
            field.getContainingClass() == containingClass);
        var interpreter = new StandardDataFlowInterpreter(flow, DfaListener.EMPTY) {
          final Map<PsiField, Boolean> map = new HashMap<>();
          private boolean isCallExposingNonInitializedFields(Instruction instruction) {
            if (!(instruction instanceof MethodCallInstruction)) {
              return false;
            }

            PsiCall call = ((MethodCallInstruction)instruction).getCallExpression();
            if (call == null) return false;

            if (call instanceof PsiNewExpression && canAccessFields((PsiExpression)call)) {
              return true;
            }

            if (call instanceof PsiMethodCallExpression) {
              PsiExpression qualifier = ((PsiMethodCallExpression)call).getMethodExpression().getQualifierExpression();
              if (qualifier == null || canAccessFields(qualifier)) {
                return true;
              }
            }

            PsiExpressionList argumentList = call.getArgumentList();
            if (argumentList != null) {
              for (PsiExpression expression : argumentList.getExpressions()) {
                if (canAccessFields(expression)) return true;
              }
            }

            return false;
          }

          private boolean canAccessFields(PsiExpression expression) {
            PsiClass type = PsiUtil.resolveClassInClassTypeOnly(expression.getType());
            JBIterable<PsiClass> typeContainers =
              JBIterable.generate(type, PsiClass::getContainingClass).takeWhile(c -> !c.hasModifierProperty(PsiModifier.STATIC));
            return typeContainers.contains(containingClass);
          }

          @Override
          protected DfaInstructionState @NotNull [] acceptInstruction(@NotNull DfaInstructionState instructionState) {
            Instruction instruction = instructionState.getInstruction();
            if ((isCallExposingNonInitializedFields(instruction) || instruction instanceof ReturnInstruction)) {
              for (PsiField field : containingClass.getFields()) {
                DfaVariableValue value = PlainDescriptor.createVariableValue(getFactory(), field);
                DfType dfType = instructionState.getMemoryState().getDfType(value);
                if (dfType.isSuperType(DfTypes.NULL)) {
                  map.put(field, false);
                }
                else if (!map.containsKey(field)) {
                  map.put(field, true);
                }
              }
              return DfaInstructionState.EMPTY_ARRAY;
            }
            return super.acceptInstruction(instructionState);
          }
        };
        DfaMemoryState state = DfaUtil.createStateWithEnabledAssertions(factory);
        final RunnerResult rc = interpreter.interpret(state);
        Set<PsiField> notNullFields = new HashSet<>();
        if (rc == RunnerResult.OK) {
          interpreter.map.forEach((key, value) -> {
            if (value) {
              notNullFields.add(key);
            }
          });
        }
        return Result.create(notNullFields, body, PsiModificationTracker.MODIFICATION_COUNT);
      }
    });
  }

  /**
   * Finds all initializers of the specified field inside constructors and class/instance initializers.
   * Includes the field's own initializer, if any.
   *
   * @param field  the field find initializers for
   * @return the initializers found.
   */
  public static @NotNull List<PsiExpression> findAllConstructorInitializers(@NotNull PsiField field) {
    final List<PsiExpression> result = ContainerUtil.createLockFreeCopyOnWriteList();
    ContainerUtil.addIfNotNull(result, field.getInitializer());

    final PsiClass containingClass = field.getContainingClass();
    if (containingClass != null && !(containingClass instanceof PsiCompiledElement)) {
      result.addAll(getAllConstructorFieldInitializers(containingClass).get(field));
    }
    return result;
  }

  /**
   * Finds all initializers of the specified record component.
   * This includes the right-side of assignments in a compact constructor to the implicit parameter
   * corresponding to the specified component.
   * Since a record component is implicitly final, all of its initializers will be inside a constructor (when the code is compilable).
   *
   * @param component  the record component to find initializers for
   * @return the initializers found.
   */
  static @NotNull List<PsiExpression> findAllConstructorInitializers(@NotNull PsiRecordComponent component) {
    PsiClass containingClass = component.getContainingClass();
    return containingClass == null || containingClass instanceof PsiCompiledElement
           ? Collections.emptyList()
           : new ArrayList<>(getAllConstructorFieldInitializers(containingClass).get(component));
  }

  private static MultiMap<PsiVariable, PsiExpression> getAllConstructorFieldInitializers(PsiClass psiClass) {
    if (psiClass instanceof PsiCompiledElement) {
      return MultiMap.empty();
    }

    return CachedValuesManager.getCachedValue(psiClass, new CachedValueProvider<>() {
      @Override
      public @NotNull Result<MultiMap<PsiVariable, PsiExpression>> compute() {
        final Set<String> fieldNames = new HashSet<>();
        for (PsiField field : psiClass.getFields()) {
          ContainerUtil.addIfNotNull(fieldNames, field.getName());
        }

        final MultiMap<PsiVariable, PsiExpression> result = new MultiMap<>();
        JavaRecursiveElementWalkingVisitor visitor = new JavaRecursiveElementWalkingVisitor() {
          @Override
          public void visitAssignmentExpression(@NotNull PsiAssignmentExpression assignment) {
            super.visitAssignmentExpression(assignment);
            PsiExpression rExpression = assignment.getRExpression();
            if (rExpression != null &&
                assignment.getLExpression() instanceof PsiReferenceExpression ref &&
                fieldNames.contains(ref.getReferenceName())) {
              PsiElement target = ref.resolve();
              if (target instanceof PsiField field && field.getContainingClass() == psiClass) {
                result.putValue(field instanceof LightRecordField f ? f.getRecordComponent() : field, rExpression);
              }
              else if (target instanceof LightCompactConstructorParameter parameter) {
                result.putValue(parameter.getRecordComponent(), rExpression);
              }
            }
          }
        };

        for (PsiMethod constructor : psiClass.getConstructors()) {
          if (constructor.getLanguage().isKindOf(JavaLanguage.INSTANCE)) {
            constructor.accept(visitor);
          }
        }

        for (PsiClassInitializer initializer : psiClass.getInitializers()) {
          if (initializer.getLanguage().isKindOf(JavaLanguage.INSTANCE)) {
            initializer.accept(visitor);
          }
        }

        return Result.create(result, psiClass);
      }
    });
  }

  public static @Nullable PsiElement getTopmostBlockInSameClass(@NotNull PsiElement position) {
    return JBIterable.
      generate(position, PsiElement::getParent).
      takeWhile(e -> !(e instanceof PsiMember || e instanceof PsiFile || e instanceof PsiLambdaExpression)).
      filter(e -> e instanceof PsiCodeBlock || e instanceof PsiExpression && e.getParent() instanceof PsiLambdaExpression).
      last();
  }

  public static @NotNull Collection<PsiExpression> getVariableAssignmentsInFile(@NotNull PsiVariable psiVariable,
                                                                                final boolean literalsOnly,
                                                                                final PsiElement place) {
    Ref<Boolean> modificationRef = Ref.create(Boolean.FALSE);
    PsiElement codeBlock = place == null? null : getTopmostBlockInSameClass(place);
    int placeOffset = codeBlock != null? place.getTextRange().getStartOffset() : 0;
    PsiFile containingFile = psiVariable.getContainingFile();
    LocalSearchScope scope = new LocalSearchScope(new PsiElement[]{containingFile}, null, true);
    Collection<PsiReference> references = ReferencesSearch.search(psiVariable, scope).findAll();
    List<PsiExpression> list = ContainerUtil.mapNotNull(
      references,
      (NullableFunction<PsiReference, PsiExpression>)psiReference -> {
        if (modificationRef.get()) return null;
        final PsiElement parent = psiReference.getElement().getParent();
        if (parent instanceof PsiAssignmentExpression assignmentExpression) {
          final IElementType operation = assignmentExpression.getOperationTokenType();
          if (assignmentExpression.getLExpression() == psiReference) {
            if (JavaTokenType.EQ.equals(operation)) {
              final PsiExpression rValue = assignmentExpression.getRExpression();
              if (!literalsOnly || allOperandsAreLiterals(rValue)) {
                // if there's a codeBlock omit the values assigned later
                if (PsiTreeUtil.isAncestor(codeBlock, parent, true)
                    && placeOffset < parent.getTextRange().getStartOffset()) {
                  return null;
                }
                return rValue;
              }
              else {
                modificationRef.set(Boolean.TRUE);
              }
            }
            else if (JavaTokenType.PLUSEQ.equals(operation)) {
              modificationRef.set(Boolean.TRUE);
            }
          }
        }
        return null;
      });
    if (modificationRef.get()) return Collections.emptyList();
    PsiExpression initializer = psiVariable.getInitializer();
    if (initializer != null && (!literalsOnly || allOperandsAreLiterals(initializer))) {
      list = ContainerUtil.concat(list, Collections.singletonList(initializer));
    }
    return list;
  }

  private static boolean allOperandsAreLiterals(final @Nullable PsiExpression expression) {
    if (expression == null) return false;
    if (expression instanceof PsiLiteralExpression) return true;
    if (expression instanceof PsiPolyadicExpression) {
      Stack<PsiExpression> stack = new Stack<>();
      stack.add(expression);
      while (!stack.isEmpty()) {
        PsiExpression psiExpression = stack.pop();
        if (psiExpression instanceof PsiPolyadicExpression binaryExpression) {
          for (PsiExpression op : binaryExpression.getOperands()) {
            stack.push(op);
          }
        }
        else if (!(psiExpression instanceof PsiLiteralExpression)) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  /**
   * @param method method to check
   * @return nullability of vararg parameter component; {@link Nullability#UNKNOWN} if not specified or method is not vararg method.
   */
  static @NotNull Nullability getVarArgComponentNullability(PsiMethod method) {
    if (method != null) {
      if (NON_NULL_VAR_ARG.methodMatches(method)) {
        return Nullability.NOT_NULL;
      }
      PsiParameter varArg = ArrayUtil.getLastElement(method.getParameterList().getParameters());
      if (varArg != null) {
        PsiType type = varArg.getType();
        if (type instanceof PsiEllipsisType) {
          PsiType componentType = ((PsiEllipsisType)type).getComponentType();
          return getTypeNullability(componentType);
        }
      }
    }
    return Nullability.UNKNOWN;
  }

  /**
   * Try to restore type parameters based on the expression type
   *
   * @param expression expression which type is a supertype of the type to generify
   * @param type a type to generify
   * @return a generified type, or original type if generification is not possible
   */
  public static PsiType tryGenerify(PsiExpression expression, PsiType type) {
    if (!(type instanceof PsiClassType classType)) {
      return type;
    }
    if (!classType.isRaw()) {
      return classType;
    }
    PsiClass psiClass = classType.resolve();
    if (psiClass == null) return classType;
    if (!(expression.getType() instanceof PsiClassType expressionType)) return classType;
    PsiClassType result = GenericsUtil.getExpectedGenericType(expression, psiClass, expressionType);
    if (result.isRaw()) {
      PsiClass aClass = result.resolve();
      if (aClass != null) {
        int length = aClass.getTypeParameters().length;
        PsiWildcardType wildcard = PsiWildcardType.createUnbounded(aClass.getManager());
        PsiType[] arguments = new PsiType[length];
        Arrays.fill(arguments, wildcard);
        return JavaPsiFacade.getElementFactory(aClass.getProject()).createType(aClass, arguments);
      }
    }
    return result;
  }

  /**
   * @param expr literal to create a constant type from
   * @return a DfType that describes given literal
   */
  public static @NotNull DfType fromLiteral(@NotNull PsiLiteralExpression expr) {
    PsiType type = expr.getType();
    if (type == null) return DfType.TOP;
    if (PsiTypes.nullType().equals(type)) return DfTypes.NULL;
    Object value = expr.getValue();
    if (value == null) return DfTypes.typedObject(type, Nullability.NOT_NULL);
    return DfTypes.constant(value, type);
  }

  /**
   * Create RelationType from Java token
   * @param type Java token
   * @return RelationType
   */
  public static @Nullable RelationType getRelationByToken(IElementType type) {
    if(JavaTokenType.EQEQ.equals(type)) {
      return RelationType.EQ;
    }
    if(JavaTokenType.NE.equals(type)) {
      return RelationType.NE;
    }
    if(JavaTokenType.LT.equals(type)) {
      return RelationType.LT;
    }
    if(JavaTokenType.GT.equals(type)) {
      return RelationType.GT;
    }
    if(JavaTokenType.LE.equals(type)) {
      return RelationType.LE;
    }
    if(JavaTokenType.GE.equals(type)) {
      return RelationType.GE;
    }
    if(JavaTokenType.INSTANCEOF_KEYWORD.equals(type)) {
      return RelationType.IS;
    }
    return null;
  }

  /**
