// 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.java.codeserver.highlighting;

import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.core.JavaPsiBundle;
import com.intellij.java.codeserver.core.JavaPsiReferenceUtil;
import com.intellij.java.codeserver.highlighting.errors.*;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.pom.java.JavaFeature;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.ControlFlowUtil;
import com.intellij.psi.impl.IncompleteModelUtil;
import com.intellij.psi.impl.source.resolve.graphInference.InferenceSession;
import com.intellij.psi.impl.source.resolve.graphInference.PsiPolyExpressionUtil;
import com.intellij.psi.infos.MethodCandidateInfo;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ImplicitClassSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.refactoring.util.RefactoringChangeUtil;
import com.intellij.util.JavaPsiConstructorUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;

final class ExpressionChecker {
  private final @NotNull JavaErrorVisitor myVisitor;
  private final Map<PsiElement, PsiMethod> myInsideConstructorOfClassCache = new HashMap<>(); // null value means "cached but no corresponding ctr found"

  ExpressionChecker(@NotNull JavaErrorVisitor visitor) { myVisitor = visitor; }

  void checkQualifiedNew(@NotNull PsiNewExpression expression, @Nullable PsiType type, @Nullable PsiClass aClass) {
    PsiExpression qualifier = expression.getQualifier();
    if (qualifier == null) return;
    if (type instanceof PsiArrayType) {
      myVisitor.report(JavaErrorKinds.NEW_EXPRESSION_QUALIFIED_MALFORMED.create(expression));
      return;
    }
    if (aClass == null) return;
    if (aClass.hasModifierProperty(PsiModifier.STATIC)) {
      myVisitor.report(JavaErrorKinds.NEW_EXPRESSION_QUALIFIED_STATIC_CLASS.create(expression, aClass));
      return;
    }
    if (aClass instanceof PsiAnonymousClass anonymousClass) {
      PsiClass baseClass = PsiUtil.resolveClassInType(anonymousClass.getBaseClassType());
      if (baseClass != null && baseClass.isInterface()) {
        myVisitor.report(JavaErrorKinds.NEW_EXPRESSION_QUALIFIED_ANONYMOUS_IMPLEMENTS_INTERFACE.create(expression, aClass));
        return;
      }
    }
    PsiJavaCodeReferenceElement reference = expression.getClassOrAnonymousClassReference();
    if (reference != null) {
      PsiElement refQualifier = reference.getQualifier();
      if (refQualifier != null) {
        myVisitor.report(JavaErrorKinds.NEW_EXPRESSION_QUALIFIED_QUALIFIED_CLASS_REFERENCE.create(refQualifier));
      }
    }
  }

  void checkCreateInnerClassFromStaticContext(@NotNull PsiNewExpression expression, @NotNull PsiClass aClass) {
    if (aClass instanceof PsiAnonymousClass anonymousClass) {
      aClass = anonymousClass.getBaseClassType().resolve();
      if (aClass == null) return;
    }

    PsiExpression qualifier = expression.getQualifier();
    PsiElement placeToSearchEnclosingFrom;
    if (qualifier != null) {
      placeToSearchEnclosingFrom = PsiUtil.resolveClassInType(qualifier.getType());
    }
    else {
      placeToSearchEnclosingFrom = expression;
    }
    if (placeToSearchEnclosingFrom == null) {
      return;
    }
    checkCreateInnerClassFromStaticContext(expression, placeToSearchEnclosingFrom, aClass);
  }

  void checkCreateInnerClassFromStaticContext(@NotNull PsiElement element,
                                              @NotNull PsiElement placeToSearchEnclosingFrom,
                                              @NotNull PsiClass aClass) {
    if (aClass.hasModifierProperty(PsiModifier.STATIC)) return;
    PsiClass outerClass = aClass.getContainingClass();
    if (outerClass == null) {
      if (!(aClass.getParent() instanceof PsiDeclarationStatement)) return; // local class
      PsiMember scope = PsiTreeUtil.getParentOfType(aClass, PsiMember.class);
      if (scope == null) return;
      if (scope.hasModifierProperty(PsiModifier.STATIC)) {
        PsiModifierListOwner enclosingStaticElement = PsiUtil.getEnclosingStaticElement(element, null);
        assert enclosingStaticElement != null;
        if (enclosingStaticElement != scope) {
          myVisitor.report(JavaErrorKinds.INSTANTIATION_LOCAL_CLASS_WRONG_STATIC_CONTEXT.create(element, aClass));
        }
        return;
      }
      outerClass = scope.getContainingClass();
      if (outerClass == null) return;
    }

    if (outerClass instanceof PsiSyntheticClass ||
        InheritanceUtil.hasEnclosingInstanceInScope(outerClass, placeToSearchEnclosingFrom, true, false)) {
      return;
    }
    checkIllegalEnclosingUsage(placeToSearchEnclosingFrom, aClass, outerClass, element);
  }

  void checkIllegalEnclosingUsage(@NotNull PsiElement place,
                                  @Nullable PsiClass aClass,
                                  @NotNull PsiClass outerClass,
                                  @NotNull PsiElement elementToHighlight) {
    var context = new JavaErrorKinds.ClassStaticReferenceErrorContext(outerClass, aClass, place);
    if (!PsiTreeUtil.isContextAncestor(outerClass, place, false)) {
      myVisitor.report(JavaErrorKinds.CLASS_NOT_ENCLOSING.create(elementToHighlight, context));
    }
    else if (context.enclosingStaticElement() != null) {
      myVisitor.report(JavaErrorKinds.CLASS_CANNOT_BE_REFERENCED_FROM_STATIC_CONTEXT.create(elementToHighlight, context));
    }
  }

  /**
   * @return true if there's no assignability problem reported
   */
  boolean checkAssignability(@Nullable PsiType lType,
                             @Nullable PsiType rType,
                             @Nullable PsiExpression expression,
                             @NotNull PsiElement elementToHighlight) {
    if (lType == rType) return true;
    if (expression == null) {
      if (rType == null || lType == null || TypeConversionUtil.isAssignable(lType, rType)) return true;
    }
    else if (TypeConversionUtil.areTypesAssignmentCompatible(lType, expression) || PsiTreeUtil.hasErrorElements(expression)) {
      return true;
    }
    if (rType == null) {
      rType = expression.getType();
    }
    if (lType == null || lType == PsiTypes.nullType()) {
      return true;
    }
    if (expression != null && myVisitor.isIncompleteModel() && IncompleteModelUtil.isPotentiallyConvertible(lType, expression)) {
      return true;
    }
    return myVisitor.reportIncompatibleType(lType, rType, elementToHighlight);
  }

  void checkMustBeBoolean(@NotNull PsiExpression expr) {
    PsiElement parent = expr.getParent();
    if (parent instanceof PsiIfStatement ||
        parent instanceof PsiConditionalLoopStatement statement && expr.equals(statement.getCondition()) ||
        parent instanceof PsiConditionalExpression condExpr && condExpr.getCondition() == expr) {
      if (expr.getNextSibling() instanceof PsiErrorElement) return;

      PsiType type = expr.getType();
      if (!TypeConversionUtil.isBooleanType(type) && !PsiTreeUtil.hasErrorElements(expr)) {
        if (type == null && myVisitor.isIncompleteModel() && IncompleteModelUtil.mayHaveUnknownTypeDueToPendingReference(expr)) {
          return;
        }
        myVisitor.reportIncompatibleType(PsiTypes.booleanType(), type, expr);
      }
    }
  }

  void checkArrayInitializer(@NotNull PsiExpression initializer, @NotNull PsiArrayInitializerExpression initializerList) {
    PsiType arrayType = initializerList.getType();
    if (!(arrayType instanceof PsiArrayType theArrayType)) return;
    PsiType componentType = theArrayType.getComponentType();
    PsiType initializerType = initializer.getType();
    checkArrayInitializerCompatibleTypes(initializer, initializerType, componentType);
  }

  private void checkArrayInitializerCompatibleTypes(@NotNull PsiExpression initializer,
                                                    @Nullable PsiType initializerType,
                                                    @NotNull PsiType componentType) {
    if (initializerType == null) {
      if (myVisitor.isIncompleteModel() && IncompleteModelUtil.mayHaveUnknownTypeDueToPendingReference(initializer)) return;
      myVisitor.report(JavaErrorKinds.ARRAY_ILLEGAL_INITIALIZER.create(initializer, componentType));
    } else {
      PsiExpression expression = initializer instanceof PsiArrayInitializerExpression ? null : initializer;
      checkAssignability(componentType, initializerType, expression, initializer);
    }
  }

  void checkExpressionRequired(@NotNull PsiReferenceExpression expression, @NotNull JavaResolveResult resultForIncompleteCode) {
    if (expression.getNextSibling() instanceof PsiErrorElement) return;

    PsiElement resolved = resultForIncompleteCode.getElement();
    if (resolved == null || resolved instanceof PsiVariable) return;

    PsiElement parent = expression.getParent();
    if (parent instanceof PsiReferenceExpression || parent instanceof PsiMethodCallExpression || parent instanceof PsiBreakStatement) {
      return;
    }
    myVisitor.report(JavaErrorKinds.EXPRESSION_EXPECTED.create(expression));
  }

  void checkArrayInitializerApplicable(@NotNull PsiArrayInitializerExpression expression) {
    /*
    JLS 10.6 Array Initializers
    An array initializer may be specified in a declaration, or as part of an array creation expression
    */
    PsiElement parent = expression.getParent();
    if (parent instanceof PsiVariable variable) {
      PsiTypeElement typeElement = variable.getTypeElement();
      boolean isInferredType = typeElement != null && typeElement.isInferredType();
      if (!isInferredType && variable.getType() instanceof PsiArrayType) return;
    }
    else if (parent instanceof PsiNewExpression || parent instanceof PsiArrayInitializerExpression) {
      return;
    }
    myVisitor.report(JavaErrorKinds.ARRAY_INITIALIZER_NOT_ALLOWED.create(expression));
  }

  void checkValidArrayAccessExpression(@NotNull PsiArrayAccessExpression arrayAccessExpression) {
    PsiExpression arrayExpression = arrayAccessExpression.getArrayExpression();
    PsiType arrayExpressionType = arrayExpression.getType();

    if (arrayExpressionType != null && !(arrayExpressionType instanceof PsiArrayType)) {
      myVisitor.report(JavaErrorKinds.ARRAY_TYPE_EXPECTED.create(arrayExpression, arrayExpressionType));
    }

    PsiExpression indexExpression = arrayAccessExpression.getIndexExpression();
    if (indexExpression != null) {
      checkAssignability(PsiTypes.intType(), indexExpression.getType(), indexExpression, indexExpression);
    }
  }

  /**
   * See JLS 8.3.3.
   */
  void checkIllegalForwardReferenceToField(@NotNull PsiReferenceExpression expression, @NotNull PsiField referencedField) {
    JavaPsiReferenceUtil.ForwardReferenceProblem problem = JavaPsiReferenceUtil.checkForwardReference(expression, referencedField, false);
    if (problem == JavaPsiReferenceUtil.ForwardReferenceProblem.LEGAL) return;
    var errorKind = referencedField instanceof PsiEnumConstant
                    ? (problem == JavaPsiReferenceUtil.ForwardReferenceProblem.ILLEGAL_FORWARD_REFERENCE
                       ? JavaErrorKinds.REFERENCE_ENUM_FORWARD
                       : JavaErrorKinds.REFERENCE_ENUM_SELF)
                    : (problem == JavaPsiReferenceUtil.ForwardReferenceProblem.ILLEGAL_FORWARD_REFERENCE
                       ? JavaErrorKinds.REFERENCE_FIELD_FORWARD
                       : JavaErrorKinds.REFERENCE_FIELD_SELF);
    myVisitor.report(errorKind.create(expression, referencedField));
  }

  void checkMethodCall(@NotNull PsiMethodCallExpression methodCall) {
    PsiExpressionList list = methodCall.getArgumentList();
    PsiReferenceExpression referenceToMethod = methodCall.getMethodExpression();
    
    JavaResolveResult[] results = referenceToMethod.multiResolve(true);
    JavaResolveResult resolveResult = results.length == 1 ? results[0] : JavaResolveResult.EMPTY;
    PsiElement resolved = resolveResult.getElement();

    PsiSubstitutor substitutor = resolveResult.getSubstitutor();
    if (resolved instanceof PsiMethod psiMethod && resolveResult.isValidResult()) {
      checkUnhandledExceptions(methodCall);
      if (myVisitor.hasErrorResults()) return;
      if (psiMethod.hasModifierProperty(PsiModifier.STATIC)) {
        PsiClass containingClass = psiMethod.getContainingClass();
        if (containingClass != null && containingClass.isInterface()) {
          PsiElement element = ObjectUtils.notNull(referenceToMethod.getReferenceNameElement(), referenceToMethod);
          myVisitor.checkFeature(element, JavaFeature.STATIC_INTERFACE_CALLS);
          if (myVisitor.hasErrorResults()) return;
          checkStaticInterfaceCallQualifier(referenceToMethod, resolveResult, containingClass);
        }
      }
      MethodCandidateInfo methodInfo = (MethodCandidateInfo)resolveResult;
      myVisitor.myGenericsChecker.checkInferredIntersections(substitutor, methodCall);
      if (myVisitor.hasErrorResults()) return;
      checkVarargParameterErasureToBeAccessible(methodInfo, methodCall);
      if (myVisitor.hasErrorResults()) return;
      checkIncompatibleType(methodCall, methodInfo, methodCall);
      if (myVisitor.hasErrorResults()) return;
      checkInferredReturnTypeAccessible(methodInfo, methodCall);
    }
    else {
      MethodCandidateInfo candidateInfo = resolveResult instanceof MethodCandidateInfo ? (MethodCandidateInfo)resolveResult : null;
      PsiMethod resolvedMethod = candidateInfo != null ? candidateInfo.getElement() : null;

      if (resolveResult.isAccessible() && resolveResult.isStaticsScopeCorrect()) {
        if (candidateInfo != null && !candidateInfo.isApplicable()) {
          if (candidateInfo.isTypeArgumentsApplicable()) {
            checkIncompatibleCall(list, candidateInfo);
          }
          else {
            PsiReferenceParameterList typeArgumentList = methodCall.getTypeArgumentList();
            PsiSubstitutor applicabilitySubstitutor = candidateInfo.getSubstitutor(false);
            if (typeArgumentList.getTypeArguments().length == 0 && resolvedMethod.hasTypeParameters()) {
              checkInferredTypeArguments(resolvedMethod, methodCall, applicabilitySubstitutor);
            }
            else {
              myVisitor.myGenericsChecker.checkParameterizedReferenceTypeArguments(resolved, referenceToMethod, applicabilitySubstitutor);
            }
          }
        }
        else {
          myVisitor.report(JavaErrorKinds.CALL_EXPECTED.create(methodCall));
        }
      }
    }
    if (!myVisitor.hasErrorResults()) {
      myVisitor.myGenericsChecker.checkParameterizedReferenceTypeArguments(resolved, referenceToMethod, substitutor);
    }
  }

  void checkInferredTypeArguments(@NotNull PsiTypeParameterListOwner listOwner,
                                  @NotNull PsiMethodCallExpression call,
                                  @NotNull PsiSubstitutor substitutor) {
    PsiTypeParameter[] typeParameters = listOwner.getTypeParameters();
    Pair<PsiTypeParameter, PsiType> inferredTypeArgument = GenericsUtil.findTypeParameterWithBoundError(
      typeParameters, substitutor, call, false);
    if (inferredTypeArgument != null) {
      PsiType extendsType = inferredTypeArgument.second;
      PsiTypeParameter typeParameter = inferredTypeArgument.first;
      PsiClass boundClass = extendsType instanceof PsiClassType classType ? classType.resolve() : null;

      var kind = boundClass == null || typeParameter.isInterface() == boundClass.isInterface()
                 ? JavaErrorKinds.TYPE_PARAMETER_INFERRED_TYPE_NOT_WITHIN_EXTEND_BOUND
                 : JavaErrorKinds.TYPE_PARAMETER_INFERRED_TYPE_NOT_WITHIN_IMPLEMENT_BOUND;
      myVisitor.report(kind.create(call, new JavaErrorKinds.TypeParameterBoundMismatchContext(
        typeParameter, extendsType, requireNonNull(substitutor.substitute(typeParameter)))));
    }
  }

  private void checkIncompatibleCall(@NotNull PsiExpressionList list, @NotNull MethodCandidateInfo candidateInfo) {
    if (PsiTreeUtil.hasErrorElements(list)) return;
    JavaMismatchedCallContext context = JavaMismatchedCallContext.create(list, candidateInfo);
    List<PsiExpression> mismatchedExpressions = context.mismatchedExpressions();
    if (mismatchedExpressions.isEmpty() && myVisitor.isIncompleteModel()) return;

    if (mismatchedExpressions.size() == list.getExpressions().length || mismatchedExpressions.isEmpty()) {
      PsiElement anchor = list.getTextRange().isEmpty() ? ObjectUtils.notNull(list.getPrevSibling(), list) : list;
      if (!mismatchedExpressions.isEmpty() && !context.argCountMismatch() &&
          ContainerUtil.and(mismatchedExpressions, e -> e.getType() instanceof PsiLambdaParameterType)) {
        // Likely, the problem is induced
        return;
      }
      myVisitor.report(JavaErrorKinds.CALL_WRONG_ARGUMENTS.create(anchor, context));
    }
    else {
      for (PsiExpression wrongArg : mismatchedExpressions) {
        if (wrongArg.getType() instanceof PsiLambdaParameterType) continue;
        myVisitor.report(JavaErrorKinds.CALL_WRONG_ARGUMENTS.create(wrongArg, context));
      }
    }
  }

  private void checkInferredReturnTypeAccessible(@NotNull MethodCandidateInfo info, @NotNull PsiMethodCallExpression methodCall) {
    PsiMethod method = info.getElement();
    PsiClass targetClass = PsiUtil.resolveClassInClassTypeOnly(method.getReturnType());
    if (targetClass instanceof PsiTypeParameter typeParameter && typeParameter.getOwner() == method) {
      PsiClass targetType = PsiUtil.resolveClassInClassTypeOnly(InferenceSession.getTargetTypeByParent(methodCall));
      if (targetType != null && !PsiUtil.isAccessible(targetType, methodCall, null)) {
        myVisitor.report(JavaErrorKinds.TYPE_INACCESSIBLE.create(methodCall.getArgumentList(), targetType));
      }
    }
  }

  void checkTemplateExpression(@NotNull PsiTemplateExpression templateExpression) {
    myVisitor.checkFeature(templateExpression, JavaFeature.STRING_TEMPLATES);
    if (myVisitor.hasErrorResults()) return;
    PsiExpression processor = templateExpression.getProcessor();
    if (processor == null) {
      myVisitor.report(JavaErrorKinds.STRING_TEMPLATE_PROCESSOR_MISSING.create(templateExpression));
      return;
    }
    PsiType type = processor.getType();
    if (type == null) return;

    PsiElementFactory factory = myVisitor.factory();
    PsiClassType processorType = factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_STRING_TEMPLATE_PROCESSOR, processor.getResolveScope());
    if (!TypeConversionUtil.isAssignable(processorType, type)) {
      if (myVisitor.isIncompleteModel() && IncompleteModelUtil.isPotentiallyConvertible(processorType, processor)) return;
      myVisitor.reportIncompatibleType(processorType, type, processor);
      return;
    }

    PsiClass processorClass = processorType.resolve();
    if (processorClass == null) return;
    for (PsiClassType classType : PsiTypesUtil.getClassTypeComponents(type)) {
      if (!TypeConversionUtil.isAssignable(processorType, classType)) continue;
      PsiClassType.ClassResolveResult resolveResult = classType.resolveGenerics();
      PsiClass aClass = resolveResult.getElement();
      if (aClass == null) continue;
      PsiSubstitutor substitutor = TypeConversionUtil.getClassSubstitutor(processorClass, aClass, resolveResult.getSubstitutor());
      if (substitutor == null) continue;
      Map<PsiTypeParameter, PsiType> substitutionMap = substitutor.getSubstitutionMap();
      if (substitutionMap.isEmpty() || substitutionMap.containsValue(null)) {
        myVisitor.report(JavaErrorKinds.STRING_TEMPLATE_RAW_PROCESSOR.create(processor, type));
        return;
      }
    }
  }

  void checkNewExpression(@NotNull PsiNewExpression expression, PsiType type) {
    if (!(type instanceof PsiClassType classType)) return;
    PsiClassType.ClassResolveResult typeResult = classType.resolveGenerics();
    PsiClass aClass = typeResult.getElement();
    if (aClass instanceof PsiAnonymousClass anonymousClass) {
      classType = anonymousClass.getBaseClassType();
      typeResult = classType.resolveGenerics();
      aClass = typeResult.getElement();
    }
    if (aClass == null || aClass instanceof PsiTypeParameter) return;

    PsiJavaCodeReferenceElement classReference = expression.getClassOrAnonymousClassReference();
    checkConstructorCall(typeResult, expression, classReference);
  }

  void checkAmbiguousConstructorCall(@NotNull PsiJavaCodeReferenceElement ref, PsiElement resolved) {
    if (resolved instanceof PsiClass psiClass &&
        ref.getParent() instanceof PsiNewExpression newExpression && psiClass.getConstructors().length > 0) {
      PsiExpressionList argumentList = newExpression.getArgumentList();
      if (newExpression.resolveMethod() == null && !PsiTreeUtil.findChildrenOfType(argumentList, PsiFunctionalExpression.class).isEmpty()) {
        PsiType type = newExpression.getType();
        if (type instanceof PsiClassType classType) {
          checkConstructorCall(classType.resolveGenerics(), newExpression, newExpression.getClassReference());
        }
      }
    }
  }

  void checkConstructorCallProblems(@NotNull PsiMethodCallExpression methodCall) {
    if (!JavaPsiConstructorUtil.isConstructorCall(methodCall)) return;
    PsiMethod method = PsiTreeUtil.getParentOfType(methodCall, PsiMethod.class, true, PsiClass.class, PsiLambdaExpression.class);
    PsiMethod contextMethod =
      method != null ? method : PsiTreeUtil.getContextOfType(methodCall, PsiMethod.class, true, PsiClass.class, PsiLambdaExpression.class);
    if (contextMethod == null || !contextMethod.isConstructor()) {
      myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_ONLY_ALLOWED_IN_CONSTRUCTOR.create(methodCall));
      return;
    }
    if (JavaPsiRecordUtil.isCompactConstructor(contextMethod) || JavaPsiRecordUtil.isExplicitCanonicalConstructor(contextMethod)) {
      myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_RECORD_IN_CANONICAL.create(methodCall));
      return;
    }
    if (method == null) {
      // Do not report other errors for detached constructor call, as they are likely non-relevant
      return;
    }
    PsiMethodCallExpression constructorCall = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(method);
    if (constructorCall != methodCall) {
      myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_DUPLICATE.create(methodCall));
      return;
    }
    PsiElement codeBlock = methodCall.getParent().getParent();
    if (!(codeBlock instanceof PsiCodeBlock) || !(codeBlock.getParent() instanceof PsiMethod)) {
      myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_MUST_BE_TOP_LEVEL_STATEMENT.create(methodCall));
      return;
    }
    PsiStatement prevStatement = PsiTreeUtil.getPrevSiblingOfType(methodCall.getParent(), PsiStatement.class);
    if (prevStatement != null) {
      if (!myVisitor.isApplicable(JavaFeature.STATEMENTS_BEFORE_SUPER)) {
        myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_MUST_BE_FIRST_STATEMENT.create(methodCall));
        return;
      }
    }
    if (JavaPsiConstructorUtil.isChainedConstructorCall(methodCall) && JavaPsiConstructorUtil.isRecursivelyCalledConstructor(method)) {
      myVisitor.report(JavaErrorKinds.CALL_CONSTRUCTOR_RECURSIVE.create(methodCall));
    }
  }

  void checkSuperAbstractMethodDirectCall(@NotNull PsiMethodCallExpression methodCallExpression) {
    PsiReferenceExpression expression = methodCallExpression.getMethodExpression();
    if (!(expression.getQualifierExpression() instanceof PsiSuperExpression)) return;
    PsiMethod method = methodCallExpression.resolveMethod();
    if (method != null && method.hasModifierProperty(PsiModifier.ABSTRACT)) {
      myVisitor.report(JavaErrorKinds.CALL_DIRECT_ABSTRACT_METHOD_ACCESS.create(methodCallExpression, method));
    }
  }

  private @Nullable PsiClass checkQualifier(@NotNull PsiQualifiedExpression expr) {
    PsiJavaCodeReferenceElement qualifier = expr.getQualifier();
    if (qualifier != null) {
      PsiElement resolved = qualifier.advancedResolve(true).getElement();
      if (resolved instanceof PsiClass cls) {
        return cls;
      }
      if (resolved != null) {
        myVisitor.report(JavaErrorKinds.EXPRESSION_QUALIFIED_CLASS_EXPECTED.create(qualifier));
      }
      return null;
    }
    return PsiUtil.getContainingClass(expr);
  }

  void checkThisExpressionInIllegalContext(@NotNull PsiThisExpression expr) {
    PsiClass aClass = checkQualifier(expr);
    if (aClass == null) return;

    if (!InheritanceUtil.hasEnclosingInstanceInScope(aClass, expr, false, false)) {
      checkIllegalEnclosingUsage(expr, null, aClass, expr);
    }
  }

  void checkSuperExpressionInIllegalContext(@NotNull PsiSuperExpression expr) {
    PsiJavaCodeReferenceElement qualifier = expr.getQualifier();
    PsiElement parent = expr.getParent();
    if (!(parent instanceof PsiReferenceExpression ref)) {
      // like in 'Object o = super;'
      myVisitor.report(JavaErrorKinds.EXPRESSION_SUPER_DOT_EXPECTED.create(expr));
      return;
    }

    PsiClass aClass = checkQualifier(expr);
    if (aClass == null) return;

    boolean extensionQualifier = qualifier != null && myVisitor.isApplicable(JavaFeature.EXTENSION_METHODS);

    if (!InheritanceUtil.hasEnclosingInstanceInScope(aClass, expr, false, false)) {
      boolean resolvesToImmediateSuperInterface =
        extensionQualifier && aClass.equals(PsiUtil.resolveClassInClassTypeOnly(expr.getType())) &&
        PsiUtil.getEnclosingStaticElement(expr, PsiUtil.getContainingClass(expr)) == null;
      if (!resolvesToImmediateSuperInterface) {
        checkIllegalEnclosingUsage(expr, null, aClass, expr);
        if (myVisitor.hasErrorResults()) return;
      }
      PsiElement resolved = ref.resolve();
      //15.11.2
      //The form T.super.Identifier refers to the field named Identifier of the lexically enclosing instance corresponding to T,
      //but with that instance viewed as an instance of the superclass of T.
      if (resolved instanceof PsiField) {
        myVisitor.report(JavaErrorKinds.EXPRESSION_SUPER_NOT_ENCLOSING_CLASS.create(expr, aClass));
      }
    }
    if (extensionQualifier && aClass.isInterface()) {
      //15.12.1 for method invocation expressions; 15.13 for method references
      //If TypeName denotes an interface, I, then let T be the type declaration immediately enclosing the method reference expression.
      //It is a compile-time error if I is not a direct superinterface of T,
      //or if there exists some other direct superclass or direct superinterface of T, J, such that J is a subtype of I.
      PsiClass classT = PsiUtil.getContainingClass(expr);
      if (classT != null) {
        PsiElement resolved = ref.resolve();

        PsiClass containingClass =
          ObjectUtils.notNull(resolved instanceof PsiMethod psiMethod ? psiMethod.getContainingClass() : null, aClass);
        for (PsiClass superClass : classT.getSupers()) {
          if (superClass.isInheritor(containingClass, true)) {
            if (superClass.isInheritor(aClass, true)) {
              myVisitor.report(JavaErrorKinds.EXPRESSION_SUPER_BAD_QUALIFIER_REDUNDANT_EXTENDED.create(
                qualifier, new JavaErrorKinds.SuperclassSubclassContext(superClass, containingClass)));
              return;
            }
            if (resolved instanceof PsiMethod psiMethod &&
                MethodSignatureUtil.findMethodBySuperMethod(superClass, psiMethod, true) != resolved) {
              myVisitor.report(JavaErrorKinds.EXPRESSION_SUPER_BAD_QUALIFIER_METHOD_OVERRIDDEN.create(expr, superClass));
              return;
            }
          }
        }

        if (!classT.isInheritor(aClass, false)) {
          myVisitor.report(JavaErrorKinds.EXPRESSION_SUPER_NO_ENCLOSING_INSTANCE.create(qualifier, aClass));
        }
      }
    }
  }

  void checkAssignmentCompatibleTypes(@NotNull PsiAssignmentExpression assignment) {
    PsiExpression lExpr = assignment.getLExpression();
    PsiExpression rExpr = assignment.getRExpression();
    if (rExpr == null) return;
    PsiType lType = lExpr.getType();
    PsiType rType = rExpr.getType();
    if (rType == null) return;

    IElementType sign = assignment.getOperationTokenType();
    if (JavaTokenType.EQ.equals(sign)) {
      checkAssignability(lType, rType, rExpr, rExpr);
    }
    else {
      // 15.26.2. Compound Assignment Operators
      IElementType opSign = TypeConversionUtil.convertEQtoOperation(sign);
      PsiType type = TypeConversionUtil.calcTypeForBinaryExpression(lType, rType, opSign, true);
      if (type == null || lType == null || lType instanceof PsiLambdaParameterType || type instanceof PsiLambdaParameterType ||
          TypeConversionUtil.areTypesConvertible(type, lType)) {
        return;
      }
      if (myVisitor.isIncompleteModel() && IncompleteModelUtil.isPotentiallyConvertible(lType, rExpr)) return;
      myVisitor.reportIncompatibleType(lType, type, rExpr);
    }
  }

  void checkAssignmentOperatorApplicable(@NotNull PsiAssignmentExpression assignment) {
    PsiJavaToken operationSign = assignment.getOperationSign();
    IElementType eqOpSign = operationSign.getTokenType();
    IElementType opSign = TypeConversionUtil.convertEQtoOperation(eqOpSign);
    if (opSign == null) return;
    PsiType lType = assignment.getLExpression().getType();
    PsiExpression rExpression = assignment.getRExpression();
    if (rExpression == null) return;
    PsiType rType = rExpression.getType();
    if (!TypeConversionUtil.isBinaryOperatorApplicable(opSign, lType, rType, true)) {
      if (lType instanceof PsiLambdaParameterType || rType instanceof PsiLambdaParameterType) {
        // Unresolved lambda parameter type is used: an error for parameter should be more descriptive, so let's avoid reporting type error
        return;
      }
      myVisitor.report(JavaErrorKinds.BINARY_OPERATOR_NOT_APPLICABLE.create(
        operationSign, new JavaIncompatibleTypeErrorContext(lType, rType)));
    }
  }

  void checkUnaryOperatorApplicable(@NotNull PsiUnaryExpression unary) {
    PsiJavaToken token = unary.getOperationSign();
    PsiExpression operand = unary.getOperand();
    if (operand != null && !TypeConversionUtil.isUnaryOperatorApplicable(token, operand)) {
      PsiType type = operand.getType();
      if (type == null || type instanceof PsiLambdaParameterType) return;
      myVisitor.report(JavaErrorKinds.UNARY_OPERATOR_NOT_APPLICABLE.create(unary, type));
    }
  }

  void checkPolyadicOperatorApplicable(@NotNull PsiPolyadicExpression expression) {
    PsiExpression[] operands = expression.getOperands();

    PsiType lType = operands[0].getType();
    IElementType operationSign = expression.getOperationTokenType();
    for (int i = 1; i < operands.length; i++) {
      PsiExpression operand = operands[i];
      PsiType rType = operand.getType();
      if (lType instanceof PsiLambdaParameterType || rType instanceof PsiLambdaParameterType) return;
      if (!TypeConversionUtil.isBinaryOperatorApplicable(operationSign, lType, rType, false) &&
          !(myVisitor.isIncompleteModel() && IncompleteModelUtil.isPotentiallyConvertible(lType, rType, expression))) {
        PsiJavaToken token = expression.getTokenBeforeOperand(operand);
        assert token != null : expression;
        myVisitor.report(JavaErrorKinds.BINARY_OPERATOR_NOT_APPLICABLE.create(token, new JavaIncompatibleTypeErrorContext(lType, rType)));
        return;
      }
      lType = TypeConversionUtil.calcTypeForBinaryExpression(lType, rType, operationSign, true);
    }
  }

  /**
   * JEP 440-441
   * Any variable that is used but not declared by a guard must either be final or effectively final (4.12.4)
   * and cannot be assigned to (15.26),
   * incremented (15.14.2), or decremented (15.14.3), otherwise a compile-time error occurs
   **/
  void checkOutsideDeclaredCantBeAssignmentInGuard(@Nullable PsiExpression expressionVariable) {
    if (expressionVariable == null) return;
