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

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.codeInspection.dataFlow.*;
import com.intellij.java.codeserver.core.JavaPsiReferenceUtil;
import com.intellij.java.codeserver.core.JavaPsiReferenceUtil.ForwardReferenceProblem;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.controlFlow.ControlFlowUtil;
import com.intellij.psi.impl.source.PsiFieldImpl;
import com.intellij.psi.impl.source.tree.Factory;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.ig.callMatcher.CallMatcher;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.*;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.intellij.psi.CommonClassNames.*;

public final class ExpressionUtils {
  private static final @NonNls Set<String> IMPLICIT_TO_STRING_METHOD_NAMES =
    Set.of("append", "format", "print", "printf", "println", "valueOf");
  private static final @NonNls @Unmodifiable Set<String> convertableBoxedClassNames;

  static {
    convertableBoxedClassNames = Set.of(JAVA_LANG_BYTE,
    JAVA_LANG_CHARACTER,
    JAVA_LANG_SHORT);
  }

  private static final CallMatcher KNOWN_SIMPLE_CALLS =
    CallMatcher.staticCall(JAVA_UTIL_COLLECTIONS, "emptyList", "emptySet", "emptyIterator", "emptyMap", "emptySortedMap",
                           "emptySortedSet", "emptyListIterator").parameterCount(0);

  private static final CallMatcher GET_OR_DEFAULT = CallMatcher.instanceCall(JAVA_UTIL_MAP, "getOrDefault").parameterCount(2);

  private ExpressionUtils() {}

  public static @Nullable Object computeConstantExpression(@Nullable PsiExpression expression) {
    return computeConstantExpression(expression, false);
  }

  public static @Nullable Object computeConstantExpression(@Nullable PsiExpression expression, boolean throwConstantEvaluationOverflowException) {
    if (expression == null) {
      return null;
    }
    final Project project = expression.getProject();
    final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
    final PsiConstantEvaluationHelper constantEvaluationHelper = psiFacade.getConstantEvaluationHelper();
    return constantEvaluationHelper.computeConstantExpression(expression, throwConstantEvaluationOverflowException);
  }

  public static boolean isConstant(PsiField field) {
    if (!field.hasModifierProperty(PsiModifier.FINAL)) {
      return false;
    }
    if (CollectionUtils.isEmptyArray(field)) {
      return true;
    }
    final PsiType type = field.getType();
    return ClassUtils.isImmutable(type);
  }

  public static boolean hasExpressionCount(@Nullable PsiExpressionList expressionList, int count) {
    return ControlFlowUtils.hasChildrenOfTypeCount(expressionList, count, PsiExpression.class);
  }

  public static @Nullable PsiExpression getFirstExpressionInList(@Nullable PsiExpressionList expressionList) {
    return PsiTreeUtil.getChildOfType(expressionList, PsiExpression.class);
  }
  public static @Nullable PsiExpression getOnlyExpressionInList(@Nullable PsiExpressionList expressionList) {
    return ControlFlowUtils.getOnlyChildOfType(expressionList, PsiExpression.class);
  }

  public static boolean isDeclaredConstant(PsiExpression expression) {
    PsiField field = PsiTreeUtil.getParentOfType(expression, PsiField.class);
    if (field == null) {
      final PsiAssignmentExpression assignmentExpression =
        PsiTreeUtil.getParentOfType(expression, PsiAssignmentExpression.class);
      if (assignmentExpression == null) {
        return false;
      }
      final PsiExpression lhs = assignmentExpression.getLExpression();
      if (!(lhs instanceof PsiReferenceExpression referenceExpression)) {
        return false;
      }
      final PsiElement target = referenceExpression.resolve();
      if (!(target instanceof PsiField f)) {
        return false;
      }
      field = f;
    }
    return field.hasModifierProperty(PsiModifier.STATIC) && field.hasModifierProperty(PsiModifier.FINAL);
  }

  @Contract("null -> false")
  public static boolean isEvaluatedAtCompileTime(@Nullable PsiExpression expression) {
    if (expression instanceof PsiLiteralExpression) {
      return true;
    }
    if (expression instanceof PsiPolyadicExpression polyadicExpression) {
      for (PsiExpression operand : polyadicExpression.getOperands()) {
        if (!isEvaluatedAtCompileTime(operand)) {
          return false;
        }
      }
      return true;
    }
    if (expression instanceof PsiPrefixExpression prefixExpression) {
      return isEvaluatedAtCompileTime(prefixExpression.getOperand());
    }
    if (expression instanceof PsiReferenceExpression referenceExpression) {
      final PsiElement qualifier = referenceExpression.getQualifier();
      if (qualifier instanceof PsiThisExpression) {
        return false;
      }
      final PsiElement element = referenceExpression.resolve();
      if (element instanceof PsiVariable variable) {
        return !PsiTreeUtil.isAncestor(variable, expression, true)
               && variable.hasModifierProperty(PsiModifier.FINAL)
               && isEvaluatedAtCompileTime(PsiFieldImpl.getDetachedInitializer(variable));
      }
    }
    if (expression instanceof PsiParenthesizedExpression parenthesizedExpression) {
      return isEvaluatedAtCompileTime(parenthesizedExpression.getExpression());
    }
    if (expression instanceof PsiConditionalExpression conditionalExpression) {
      return isEvaluatedAtCompileTime(conditionalExpression.getCondition()) &&
             isEvaluatedAtCompileTime(conditionalExpression.getThenExpression()) &&
             isEvaluatedAtCompileTime(conditionalExpression.getElseExpression());
    }
    if (expression instanceof PsiTypeCastExpression typeCastExpression) {
      return hasStringType(typeCastExpression) && isEvaluatedAtCompileTime(typeCastExpression.getOperand());
    }
    return false;
  }

  public static @Nullable PsiLiteralExpression getLiteral(@Nullable PsiExpression expression) {
    expression = PsiUtil.skipParenthesizedExprDown(expression);
    if (expression instanceof PsiLiteralExpression literal) return literal;
    if (!(expression instanceof PsiTypeCastExpression typeCastExpression)) return null;
    final PsiExpression operand = PsiUtil.skipParenthesizedExprDown(typeCastExpression.getOperand());
    return operand instanceof PsiLiteralExpression literal ? literal : null;
  }

  public static boolean isLiteral(@Nullable PsiExpression expression) {
    return getLiteral(expression) != null;
  }

  public static boolean isEmptyStringLiteral(@Nullable PsiExpression expression) {
    return PsiUtil.skipParenthesizedExprDown(expression) instanceof PsiLiteralExpression literal && "\"\"".equals(literal.getText());
  }

  @Contract("null -> false")
  public static boolean isNullLiteral(@Nullable PsiExpression expression) {
    return PsiUtil.deparenthesizeExpression(expression) instanceof PsiLiteralExpression literal
           && PsiUtil.isJavaToken(literal.getFirstChild(), JavaTokenType.NULL_KEYWORD);
  }

  /**
   * Returns stream of sub-expressions of supplied expression which could be equal (by ==) to resulting
   * value of the expression. The expression value is guaranteed to be equal to one of returned sub-expressions.
   *
   * <p>
   * E.g. for {@code ((a) ? (Foo)b : (c))} the stream will contain b and c.
   * </p>
   *
   * @param expression expression to create a stream from
   * @return a new stream
   */
  public static Stream<PsiExpression> nonStructuralChildren(@NotNull PsiExpression expression) {
    return StreamEx.ofTree(expression, e -> {
        if (e instanceof PsiConditionalExpression ternary) {
          return StreamEx.of(ternary.getThenExpression(), ternary.getElseExpression()).nonNull();
        }
        if (e instanceof PsiParenthesizedExpression parens) {
          return StreamEx.ofNullable(parens.getExpression());
        }
        if (e instanceof PsiSwitchExpression switchExpression) {
          PsiCodeBlock switchBody = switchExpression.getBody();
          if (switchBody == null) return StreamEx.empty();
          List<PsiExpression> result = new ArrayList<>();
          PsiStatement[] statements = switchBody.getStatements();
          for (PsiStatement statement : statements) {
            if (statement instanceof PsiSwitchLabeledRuleStatement rule) {
              PsiStatement ruleBody = rule.getBody();
              if (ruleBody instanceof PsiBlockStatement blockStatement) {
                collectYieldExpressions(blockStatement, switchExpression, result);
              }
              else if (ruleBody instanceof PsiExpressionStatement expr) {
                result.add(expr.getExpression());
              }
            }
            else {
              collectYieldExpressions(statement, switchExpression, result);
            }
          }
          return StreamEx.of(result);
        }
        return null;
      }).remove(e -> e instanceof PsiConditionalExpression ||
                     e instanceof PsiParenthesizedExpression ||
                     e instanceof PsiSwitchExpression)
      .map(e -> {
        if (e instanceof PsiTypeCastExpression cast) {
          PsiExpression operand = cast.getOperand();
          if (operand != null && !(e.getType() instanceof PsiPrimitiveType) &&
              (!(operand.getType() instanceof PsiPrimitiveType) || PsiTypes.nullType().equals(operand.getType()))) {
            // Ignore to-primitive/from-primitive casts as they may actually change the value
            return PsiUtil.skipParenthesizedExprDown(operand);
          }
        }
        return e;
      })
      .nonNull()
      .flatMap(e -> {
        if (e instanceof PsiMethodCallExpression call && GET_OR_DEFAULT.test(call)) {
          return StreamEx.of(e, call.getArgumentList().getExpressions()[1]);
        }
        return StreamEx.of(e);
      });
  }

  private static void collectYieldExpressions(@NotNull PsiStatement statement,
                                              @NotNull PsiSwitchExpression switchExpression,
                                              @NotNull Collection<PsiExpression> result) {
    Collection<PsiYieldStatement> yields = statement instanceof PsiYieldStatement yieldStatement
                                           ? Collections.singleton(yieldStatement)
                                           : PsiTreeUtil.findChildrenOfType(statement, PsiYieldStatement.class);
    List<PsiYieldStatement> myYields = ContainerUtil.filter(yields, st -> st.findEnclosingExpression() == switchExpression);
    for (PsiYieldStatement yield : myYields) {
      ContainerUtil.addIfNotNull(result, yield.getExpression());
    }
  }

  public static boolean isZero(@Nullable PsiExpression expression) {
    if (expression == null) {
      return false;
    }
    final Object value = computeConstantExpression(expression);
    if (value == null) {
      return false;
    }
    return value instanceof Double dValue && dValue == 0.0 ||
           value instanceof Float fValue && fValue == 0.0f ||
           value instanceof Integer iValue && iValue == 0 ||
           value instanceof Long lValue && lValue == 0L ||
           value instanceof Short sValue && sValue == 0 ||
           value instanceof Character cValue && cValue == 0 ||
           value instanceof Byte bValue && bValue == 0;
  }

  public static boolean isOne(@Nullable PsiExpression expression) {
    if (expression == null) {
      return false;
    }
    final Object value = computeConstantExpression(expression);
    if (value == null) {
      return false;
    }
    //noinspection FloatingPointEquality
    return value instanceof Double dValue && dValue == 1.0 ||
           value instanceof Float fValue && fValue == 1.0f ||
           value instanceof Integer iValue && iValue == 1 ||
           value instanceof Long lValue && lValue == 1L ||
           value instanceof Short sValue && sValue == 1 ||
           value instanceof Character cValue && cValue == 1 ||
           value instanceof Byte bValue && bValue == 1;
  }

  public static boolean isNegation(@Nullable PsiExpression condition,
                                   boolean ignoreNegatedNullComparison, boolean ignoreNegatedZeroComparison) {
    condition = PsiUtil.skipParenthesizedExprDown(condition);
    if (condition instanceof PsiPrefixExpression prefixExpression) {
      final IElementType tokenType = prefixExpression.getOperationTokenType();
      return tokenType.equals(JavaTokenType.EXCL);
    }
    else if (condition instanceof PsiBinaryExpression binaryExpression) {
      final PsiExpression lhs = PsiUtil.skipParenthesizedExprDown(binaryExpression.getLOperand());
      final PsiExpression rhs = PsiUtil.skipParenthesizedExprDown(binaryExpression.getROperand());
      if (lhs == null || rhs == null) {
        return false;
      }
      final IElementType tokenType = binaryExpression.getOperationTokenType();
      if (tokenType.equals(JavaTokenType.NE)) {
        if (ignoreNegatedNullComparison) {
          final String lhsText = lhs.getText();
          final String rhsText = rhs.getText();
          if (JavaKeywords.NULL.equals(lhsText) || JavaKeywords.NULL.equals(rhsText)) {
            return false;
          }
        }
        return !(ignoreNegatedZeroComparison && (isZeroLiteral(lhs) || isZeroLiteral(rhs)));
      }
    }
    return false;
  }

  private static boolean isZeroLiteral(PsiExpression expression) {
    if (!(expression instanceof PsiLiteralExpression literal)) return false;
    final Object value = literal.getValue();
    return value instanceof Integer iValue && iValue == 0 || value instanceof Long lValue && lValue == 0L;
  }

  public static boolean isOffsetArrayAccess(@Nullable PsiExpression expression, @NotNull PsiVariable variable) {
    final PsiExpression strippedExpression = PsiUtil.skipParenthesizedExprDown(expression);
    if (!(strippedExpression instanceof PsiArrayAccessExpression arrayAccessExpression)) {
      return false;
    }
    final PsiExpression arrayExpression = arrayAccessExpression.getArrayExpression();
    if (VariableAccessUtils.variableIsUsed(variable, arrayExpression)) {
      return false;
    }
    final PsiExpression index = arrayAccessExpression.getIndexExpression();
    if (index == null) {
      return false;
    }
    return expressionIsOffsetVariableLookup(index, variable);
  }

  private static boolean expressionIsOffsetVariableLookup(@Nullable PsiExpression expression, @NotNull PsiVariable variable) {
    if (isReferenceTo(expression, variable)) {
      return true;
    }
    if (!(PsiUtil.skipParenthesizedExprDown(expression) instanceof PsiBinaryExpression binaryExpression)) {
      return false;
    }
    final IElementType tokenType = binaryExpression.getOperationTokenType();
    if (!JavaTokenType.PLUS.equals(tokenType) && !JavaTokenType.MINUS.equals(tokenType)) {
      return false;
    }
    if (expressionIsOffsetVariableLookup(binaryExpression.getLOperand(), variable)) {
      return true;
    }
    return expressionIsOffsetVariableLookup(binaryExpression.getROperand(), variable) && !JavaTokenType.MINUS.equals(tokenType);
  }

  public static boolean isVariableLessThanComparison(@Nullable PsiExpression expression, @NotNull PsiVariable variable) {
    if (!(PsiUtil.skipParenthesizedExprDown(expression) instanceof PsiBinaryExpression binaryExpression)) return false;
    final IElementType tokenType = binaryExpression.getOperationTokenType();
    if (tokenType.equals(JavaTokenType.LT) || tokenType.equals(JavaTokenType.LE)) {
      return isReferenceTo(binaryExpression.getLOperand(), variable);
    }
    else if (tokenType.equals(JavaTokenType.GT) || tokenType.equals(JavaTokenType.GE)) {
      return isReferenceTo(binaryExpression.getROperand(), variable);
    }
    return false;
  }

  /**
   * Returns true if given expression is an operand of String concatenation.
   * Also works if expression parent is {@link PsiParenthesizedExpression}.
   *
   * @param expression expression to check
   * @return true if given expression is an operand of String concatenation
   */
  public static boolean isStringConcatenationOperand(PsiExpression expression) {
    final PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent());
    if (!(parent instanceof PsiPolyadicExpression polyadicExpression)) {
      return false;
    }
    if (!JavaTokenType.PLUS.equals(polyadicExpression.getOperationTokenType())) {
      return false;
    }
    final PsiExpression[] operands = polyadicExpression.getOperands();
    if (operands.length < 2) {
      return false;
    }
    for (int i = 0; i < operands.length; i++) {
      final PsiExpression operand = operands[i];
      if (PsiUtil.skipParenthesizedExprDown(operand) == expression) {
        return i == 0 && TypeUtils.isJavaLangString(operands[1].getType());
      }
      final PsiType type = operand.getType();
      if (TypeUtils.isJavaLangString(type)) {
        return true;
      }
    }
    return false;
  }


  public static boolean hasType(@Nullable PsiExpression expression, @NonNls @NotNull String typeName) {
    if (expression == null) {
      return false;
    }
    if (typeName.equals(JAVA_LANG_STRING)) {
      if (expression instanceof PsiUnaryExpression || expression instanceof PsiInstanceOfExpression ||
          expression instanceof PsiFunctionalExpression) {
        return false;
      }
      if (expression instanceof PsiPolyadicExpression poly) {
        IElementType tokenType = poly.getOperationTokenType();
        if (!tokenType.equals(JavaTokenType.PLUS)) return false;
      }
    }
    final PsiType type = expression.getType();
    return TypeUtils.typeEquals(typeName, type);
  }

  public static boolean hasStringType(@Nullable PsiExpression expression) {
    return hasType(expression, JAVA_LANG_STRING);
  }

  /**
   * The method checks if the passed expression needs to be converted to string explicitly,
   * because the containing expression (e.g. a {@code PrintStream#println} call or string concatenation expression)
   * will convert to the string automatically.
   * <p>
   * This is the case for some StringBuilder/Buffer, PrintStream/Writer and some logging methods.
   * Otherwise, it considers the conversion necessary and returns true.
   *
   * @param expression an expression to examine
   * @param throwable  is the first parameter a conversion to string on a throwable? Either {@link Throwable#toString()}
   *                   or {@link String#valueOf(Object)}
   * @return true if the explicit conversion to string is required, otherwise - false
   */
  public static boolean isConversionToStringNecessary(PsiExpression expression, boolean throwable) {
    return isConversionToStringNecessary(expression, throwable, null);
  }

  /**
   * The method checks if the passed expression needs to be converted to string explicitly,
   * because the containing expression (e.g. a {@code PrintStream#println} call or string concatenation expression)
   * will convert to the string automatically.
   * <p>
   * This is the case for some StringBuilder/Buffer, PrintStream/Writer and some logging methods.
   * Otherwise, it considers the conversion necessary and returns true.
   *
   * @param expression an expression to examine
   * @param throwable  is the first parameter a conversion to string on a throwable? Either {@link Throwable#toString()}
   *                   or {@link String#valueOf(Object)}
   * @param type       the type of the expression converted to string
   * @return true if the explicit conversion to string is required, otherwise - false
   */
  public static boolean isConversionToStringNecessary(PsiExpression expression, boolean throwable, @Nullable PsiType type) {
    final PsiElement parent = ParenthesesUtils.getParentSkipParentheses(expression);
    if (parent instanceof PsiPolyadicExpression polyadicExpression) {
      if (!TypeUtils.typeEquals(JAVA_LANG_STRING, polyadicExpression.getType())) {
        return true;
      }
      final PsiExpression[] operands = polyadicExpression.getOperands();
      boolean expressionSeen = false;
      for (int i = 0, length = operands.length; i < length; i++) {
        final PsiExpression operand = operands[i];
        if (PsiTreeUtil.isAncestor(operand, expression, false)) {
          if (i > 0) return true;
          expressionSeen = true;
        }
        else if ((!expressionSeen || i == 1) && TypeUtils.isJavaLangString(operand.getType())) {
          return false;
        }
      }
      return true;
    }
    else if (parent instanceof PsiExpressionList expressionList) {
      final PsiElement grandParent = expressionList.getParent();
      if (!(grandParent instanceof PsiMethodCallExpression methodCallExpression)) {
        return true;
      }
      final PsiReferenceExpression methodExpression1 = methodCallExpression.getMethodExpression();
      final @NonNls String name = methodExpression1.getReferenceName();
      final PsiExpression[] expressions = expressionList.getExpressions();
      if ("insert".equals(name)) {
        if (expressions.length < 2 || !expression.equals(PsiUtil.skipParenthesizedExprDown(expressions[1]))) {
          return true;
        }
        return !isCallToMethodIn(methodCallExpression, JAVA_LANG_STRING_BUILDER, JAVA_LANG_STRING_BUFFER);
      } else if ("append".equals(name)) {
        if (expression.equals(PsiUtil.skipParenthesizedExprDown(expressions[0])) &&
            isCallToMethodIn(methodCallExpression, JAVA_LANG_STRING_BUILDER, JAVA_LANG_STRING_BUFFER)) {
          if (expressions.length == 1) {
            return false;
          }
          if (expressions.length == 3) {
            return !InheritanceUtil.isInheritor(type, JAVA_LANG_CHAR_SEQUENCE);
          }
        }
        return true;
      } else if ("print".equals(name) || "println".equals(name)) {
        return !isCallToMethodIn(methodCallExpression, JAVA_IO_PRINT_STREAM, JAVA_IO_PRINT_WRITER);
      } else if ("trace".equals(name) || "debug".equals(name) || "info".equals(name) || "warn".equals(name) || "error".equals(name)) {
        if (!isCallToMethodIn(methodCallExpression, "org.slf4j.Logger")) {
          return true;
        }
        int l = 1;
        for (int i = 0; i < expressions.length; i++) {
          final PsiExpression expression1 = expressions[i];
          if (i == 0 && TypeUtils.expressionHasTypeOrSubtype(expression1, "org.slf4j.Marker")) {
            l = 2;
          }
          if (expression1 == expression) {
            if (i < l || (throwable && i == expressions.length - 1)) {
              return true;
            }
          }
        }
      }
      else if (FormatUtils.isFormatCall(methodCallExpression)) {
        return isConversionToStringNecessary(expression, methodCallExpression);
      }
      else {
        return true;
      }
    }
    else {
      return true;
    }
    return false;
  }

  /**
   * The method checks if the passed expression is an argument of {@code formatCall} which
   * needs to be converted to string explicitly.
   *
   * @param expression an expression to examine
   * @param formatCall e.g. a {@code java.io.Console#format} call
   * @return true if the explicit conversion to string is required, otherwise - false
   */
  private static boolean isConversionToStringNecessary(PsiExpression expression, PsiMethodCallExpression formatCall) {
    PsiExpressionList expressionList = formatCall.getArgumentList();
    PsiExpression formatArgument = FormatUtils.getFormatArgument(expressionList);
    if (PsiTreeUtil.isAncestor(formatArgument, expression, false)) return true;
    if (!(expression instanceof PsiMethodCallExpression callExpression)) return false;
    PsiExpression[] expressions = formatCall.getArgumentList().getExpressions();
    int formatArgumentIndex = ArrayUtil.find(expressions, formatArgument);
    if (formatArgumentIndex == -1) return false;
    int expressionIndex = ArrayUtil.find(expressions, expression);
    if (expressionIndex != formatArgumentIndex + 1 || expressionIndex != expressions.length - 1) return false;
    PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(callExpression);
    if (qualifierCall == null || qualifierCall.getTypeArguments().length > 0) return false;
    PsiExpression qualifier = qualifierCall.getMethodExpression().getQualifierExpression();
    if (qualifier == null || !(qualifier.getType() instanceof PsiClassType type) || type.isRaw()) return false;
    PsiMethod method = qualifierCall.resolveMethod();
    if (method == null) return false;
    PsiType returnType = method.getReturnType();
    return PsiUtil.resolveClassInClassTypeOnly(returnType) instanceof PsiTypeParameter returnTypeParameter &&
           returnTypeParameter.getOwner() == method &&
           !ContainerUtil.map(method.getParameterList().getParameters(), PsiParameter::getType).contains(returnType);
  }

  private static boolean isCallToMethodIn(PsiMethodCallExpression methodCallExpression, String... classNames) {
    final PsiMethod method = methodCallExpression.resolveMethod();
    if (method == null) {
      return false;
    }
    final PsiClass containingClass = method.getContainingClass();
    if (containingClass == null) {
      return false;
    }
    return ArrayUtil.contains(containingClass.getQualifiedName(), classNames);
  }

  public static boolean isNegative(@NotNull PsiExpression expression) {
    return expression.getParent() instanceof PsiPrefixExpression prefix && JavaTokenType.MINUS.equals(prefix.getOperationTokenType());
  }

  @Contract("null, _ -> null")
  public static @Nullable PsiVariable getVariableFromNullComparison(PsiExpression expression, boolean equals) {
    final PsiReferenceExpression referenceExpression = getReferenceExpressionFromNullComparison(expression, equals);
    if (referenceExpression == null) return null;
    return referenceExpression.resolve() instanceof PsiVariable variable ? variable : null;
  }

  @Contract("null, _ -> null")
  public static @Nullable PsiReferenceExpression getReferenceExpressionFromNullComparison(PsiExpression expression, boolean equals) {
    expression = PsiUtil.skipParenthesizedExprDown(expression);
    if (!(expression instanceof PsiPolyadicExpression polyadicExpression)) {
      return null;
    }
    final IElementType tokenType = polyadicExpression.getOperationTokenType();
    if (equals) {
      if (!JavaTokenType.EQEQ.equals(tokenType)) {
        return null;
      }
    }
    else {
      if (!JavaTokenType.NE.equals(tokenType)) {
        return null;
      }
    }
    final PsiExpression[] operands = polyadicExpression.getOperands();
    if (operands.length != 2) {
      return null;
    }
    PsiExpression comparedToNull = null;
    if (PsiTypes.nullType().equals(operands[0].getType())) {
      comparedToNull = operands[1];
    }
    else if (PsiTypes.nullType().equals(operands[1].getType())) {
      comparedToNull = operands[0];
    }
    comparedToNull = PsiUtil.skipParenthesizedExprDown(comparedToNull);

    return comparedToNull instanceof PsiReferenceExpression ref ? ref : null;
  }

  /**
   * Returns the expression compared with null if the supplied {@link PsiBinaryExpression} is null check (either with {@code ==}
   * or with {@code !=}). Returns null otherwise.
   *
   * @param binOp binary expression to extract the value compared with null from
   * @return value compared with null
   */
  public static @Nullable PsiExpression getValueComparedWithNull(@NotNull PsiBinaryExpression binOp) {
    final IElementType tokenType = binOp.getOperationTokenType();
    if (!tokenType.equals(JavaTokenType.EQEQ) && !tokenType.equals(JavaTokenType.NE)) return null;
    final PsiExpression left = binOp.getLOperand();
    final PsiExpression right = binOp.getROperand();
    if (isNullLiteral(right)) return left;
    if (isNullLiteral(left)) return right;
    return null;
  }

  /**
   * Returns the expression compared with zero if the supplied {@link PsiBinaryExpression} is zero check (with {@code ==}). Returns null otherwise.
   *
   * @param binOp binary expression to extract the value compared with zero from
   * @return value compared with zero
   */
  public static @Nullable PsiExpression getValueComparedWithZero(@NotNull PsiBinaryExpression binOp) {
    return getValueComparedWithZero(binOp, JavaTokenType.EQEQ);
  }

  public static @Nullable PsiExpression getValueComparedWithZero(@NotNull PsiBinaryExpression binOp, IElementType opType) {
    if (!binOp.getOperationTokenType().equals(opType)) return null;
    PsiExpression rOperand = binOp.getROperand();
    if (rOperand == null) return null;
    PsiExpression lOperand = binOp.getLOperand();
    if (isZero(lOperand)) return rOperand;
    if (isZero(rOperand)) return lOperand;
    return null;
  }

  public static boolean isStringConcatenation(PsiElement element) {
    if (!(element instanceof PsiPolyadicExpression expression)) return false;
    final PsiType type = expression.getType();
    return type != null && type.equalsToText(JAVA_LANG_STRING);
  }

  /**
   * Returns true if the expression can be moved to earlier point in program order without possible semantic change or
   * notable performance handicap. Examples of simple expressions are:
   * <ul>
   * <li>literal (number, char, string, class literal, true, false, null)</li>
   * <li>compile-time constant</li>
   * <li>this</li>
   * <li>variable/parameter read</li>
   * <li>final field read (either static or this-qualified)</li>
   * <li>some static method calls known to return final static field (like {@code Collections.emptyList()})</li>
   * </ul>
   *
   * @param expression an expression to test (must be valid expression)
   * @return true if the supplied expression is simple
   */
  @Contract("null -> false")
  public static boolean isSafelyRecomputableExpression(@Nullable PsiExpression expression) {
    expression = PsiUtil.skipParenthesizedExprDown(expression);
    if (expression instanceof PsiLiteralExpression ||
        expression instanceof PsiThisExpression ||
        expression instanceof PsiClassObjectAccessExpression ||
        isEvaluatedAtCompileTime(expression)) {
      return true;
    }
    if (expression instanceof PsiArrayInitializerExpression initializer) {
      return ContainerUtil.all(initializer.getInitializers(), e -> isSafelyRecomputableExpression(e));
    }
    if (expression instanceof PsiConditionalExpression cond) {
      return isSafelyRecomputableExpression(cond.getCondition()) &&
             isSafelyRecomputableExpression(cond.getThenExpression()) &&
             isSafelyRecomputableExpression(cond.getElseExpression());
    }
    if (expression instanceof PsiReferenceExpression ref) {
      PsiElement target = ref.resolve();
      if (target instanceof PsiLocalVariable || target instanceof PsiParameter) return true;
      PsiExpression qualifier = ref.getQualifierExpression();
      if (target == null && qualifier == null) return true;
      if (target instanceof PsiField field) {
        if (!field.hasModifierProperty(PsiModifier.FINAL)) return false;
        return qualifier == null || qualifier instanceof PsiThisExpression || field.hasModifierProperty(PsiModifier.STATIC);
      }
    }
    if (expression instanceof PsiMethodCallExpression call) {
      return KNOWN_SIMPLE_CALLS.test(call);
    }
    return false;
  }

  /**
   * Returns assignment expression if supplied element is a statement which contains assignment expression,
   * or it's an assignment expression itself. Only simple assignments are returned (like a = b, not a+= b).
   *
   * @param element element to get assignment expression from
   * @return extracted assignment or null if assignment is not found or assignment is compound
   */
  @Contract("null -> null")
  public static @Nullable PsiAssignmentExpression getAssignment(PsiElement element) {
    if (element instanceof PsiExpressionStatement statement) {
