// 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.intention.AddAnnotationModCommandAction;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.dataFlow.NullabilityProblemKind.NullabilityProblem;
import com.intellij.codeInspection.dataFlow.fix.RedundantInstanceofFix;
import com.intellij.codeInspection.dataFlow.fix.ReplaceWithArgumentFix;
import com.intellij.codeInspection.dataFlow.fix.ReplaceWithObjectsEqualsFix;
import com.intellij.codeInspection.dataFlow.interpreter.RunnerResult;
import com.intellij.codeInspection.dataFlow.java.anchor.JavaExpressionAnchor;
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceReturnAnchor;
import com.intellij.codeInspection.dataFlow.lang.ir.ControlFlow;
import com.intellij.codeInspection.dataFlow.lang.ir.Instruction;
import com.intellij.codeInspection.dataFlow.memory.DfaMemoryState;
import com.intellij.codeInspection.dataFlow.types.DfType;
import com.intellij.codeInspection.dataFlow.types.DfTypes;
import com.intellij.codeInspection.nullable.NullableStuffInspectionBase;
import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiImplUtil;
import com.intellij.psi.util.*;
import com.intellij.util.JavaPsiConstructorUtil;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.SmartHashSet;
import com.siyeh.ig.psiutils.*;
import one.util.streamex.StreamEx;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;

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

import static com.intellij.util.ObjectUtils.tryCast;

public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspectionTool {
  private static final @NonNls String SHORT_NAME = "DataFlowIssue";
  public boolean SUGGEST_NULLABLE_ANNOTATIONS;
  public boolean TREAT_UNKNOWN_MEMBERS_AS_NULLABLE;
  public boolean IGNORE_ASSERT_STATEMENTS;
  public boolean REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER = true;
  public boolean REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL = true;
  public boolean REPORT_UNSOUND_WARNINGS = true;

  @Override
  public void writeSettings(@NotNull Element node) throws WriteExternalException {
    node.addContent(new Element("option").setAttribute("name", "SUGGEST_NULLABLE_ANNOTATIONS").setAttribute("value", String.valueOf(SUGGEST_NULLABLE_ANNOTATIONS)));
    // Preserved for serialization compatibility
    node.addContent(new Element("option").setAttribute("name", "DONT_REPORT_TRUE_ASSERT_STATEMENTS").setAttribute("value", "false"));
    if (IGNORE_ASSERT_STATEMENTS) {
      node.addContent(new Element("option").setAttribute("name", "IGNORE_ASSERT_STATEMENTS").setAttribute("value", "true"));
    }
    if (TREAT_UNKNOWN_MEMBERS_AS_NULLABLE) {
      node.addContent(new Element("option").setAttribute("name", "TREAT_UNKNOWN_MEMBERS_AS_NULLABLE").setAttribute("value", "true"));
    }
    if (!REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER) {
      node.addContent(new Element("option").setAttribute("name", "REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER").setAttribute("value", "false"));
    }
    if (!REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL) {
      node.addContent(new Element("option").setAttribute("name", "REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL").setAttribute("value", "false"));
    }
    if (!REPORT_UNSOUND_WARNINGS) {
      node.addContent(new Element("option").setAttribute("name", "REPORT_UNSOUND_WARNINGS").setAttribute("value", "false"));
    }
  }

  @Override
  public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
    return new JavaElementVisitor() {
      @Override
      public void visitClass(@NotNull PsiClass aClass) {
        if (aClass instanceof PsiTypeParameter) return;
        if (PsiUtil.isLocalOrAnonymousClass(aClass) && !(aClass instanceof PsiEnumConstantInitializer)) return;

        var runner = new StandardDataFlowRunner(holder.getProject(), ThreeState.fromBoolean(IGNORE_ASSERT_STATEMENTS));
        DataFlowInstructionVisitor visitor =
          analyzeDfaWithNestedClosures(aClass, holder, runner, Collections.singletonList(runner.createMemoryState()));
        List<DfaMemoryState> states = visitor.getEndOfInitializerStates();
        boolean physical = aClass.isPhysical();
        for (PsiMethod method : aClass.getConstructors()) {
          if (physical && !method.isPhysical()) {
            // Constructor could be provided by, e.g. Lombok plugin: ignore it, we won't report any problems inside anyway
            continue;
          }
          List<DfaMemoryState> initialStates;
          PsiMethodCallExpression call = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(method);
          if (JavaPsiConstructorUtil.isChainedConstructorCall(call) || (call == null && DfaUtil.hasImplicitImpureSuperCall(aClass, method))) {
            initialStates = Collections.singletonList(runner.createMemoryState());
          } else {
            initialStates = ContainerUtil.map(states, DfaMemoryState::createCopy);
          }
          analyzeMethod(method, runner, initialStates);
        }
      }

      @Override
      public void visitMethod(@NotNull PsiMethod method) {
        if (method.isConstructor()) return;
        var runner = new StandardDataFlowRunner(holder.getProject(), ThreeState.fromBoolean(IGNORE_ASSERT_STATEMENTS));
        analyzeMethod(method, runner, Collections.singletonList(runner.createMemoryState()));
      }

      private void analyzeMethod(PsiMethod method, StandardDataFlowRunner runner, List<DfaMemoryState> initialStates) {
        PsiCodeBlock scope = method.getBody();
        if (scope == null) return;
        PsiClass containingClass = PsiTreeUtil.getParentOfType(method, PsiClass.class);
        if (containingClass != null && PsiUtil.isLocalOrAnonymousClass(containingClass) && !(containingClass instanceof PsiEnumConstantInitializer)) return;

        analyzeDfaWithNestedClosures(scope, holder, runner, initialStates);
        analyzeNullLiteralMethodArguments(method, holder);
      }

      @Override
      public void visitMethodReferenceExpression(@NotNull PsiMethodReferenceExpression expression) {
        super.visitMethodReferenceExpression(expression);
        if (!REPORT_UNSOUND_WARNINGS) return;
        final PsiElement resolve = expression.resolve();
        if (resolve instanceof PsiMethod) {
          final PsiType methodReturnType = ((PsiMethod)resolve).getReturnType();
          if (TypeConversionUtil.isPrimitiveWrapper(methodReturnType) && NullableNotNullManager.isNullable((PsiMethod)resolve)) {
            final PsiType returnType = LambdaUtil.getFunctionalInterfaceReturnType(expression);
            if (TypeConversionUtil.isPrimitiveAndNotNull(returnType)) {
              holder.registerProblem(expression, JavaAnalysisBundle.message("dataflow.message.unboxing.method.reference"));
            }
          }
        }
      }
    };
  }

  protected LocalQuickFix createNavigateToNullParameterUsagesFix(PsiParameter parameter) {
    return null;
  }

  private void analyzeNullLiteralMethodArguments(PsiMethod method, ProblemsHolder holder) {
    if (REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER && holder.isOnTheFly()) {
      for (PsiParameter parameter : NullParameterConstraintChecker.checkMethodParameters(method)) {
        PsiIdentifier name = parameter.getNameIdentifier();
        if (name != null) {
          holder.registerProblem(name, JavaAnalysisBundle.message("dataflow.method.fails.with.null.argument"),
                                 ProblemHighlightType.GENERIC_ERROR_OR_WARNING, createNavigateToNullParameterUsagesFix(parameter));
        }
      }
    }
  }

  private DataFlowInstructionVisitor analyzeDfaWithNestedClosures(PsiElement scope,
                                                                  ProblemsHolder holder,
                                                                  StandardDataFlowRunner dfaRunner,
                                                                  Collection<? extends DfaMemoryState> initialStates) {
    DataFlowInstructionVisitor visitor = new DataFlowInstructionVisitor(TREAT_UNKNOWN_MEMBERS_AS_NULLABLE);
    ControlFlow flow = dfaRunner.buildFlow(scope);
    if (flow == null) return visitor;
    visitor.initInstanceOf(flow.getInstructions());
    RunnerResult rc = dfaRunner.analyzeFlow(scope, visitor, initialStates, flow);
    if (rc == RunnerResult.OK) {
      if (dfaRunner.wasForciblyMerged() &&
          (ApplicationManager.getApplication().isUnitTestMode() || Registry.is("ide.dfa.report.imprecise"))) {
        reportAnalysisQualityProblem(holder, scope, "dataflow.not.precise");
      }
      createDescription(holder, visitor, scope, flow.getInstructions());
      dfaRunner.forNestedClosures((closure, states) -> analyzeDfaWithNestedClosures(closure, holder, dfaRunner, states));
    }
    else if (rc == RunnerResult.TOO_COMPLEX) {
      reportAnalysisQualityProblem(holder, scope, "dataflow.too.complex");
    }
    return visitor;
  }

  private static void reportAnalysisQualityProblem(ProblemsHolder holder, PsiElement scope, @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String problemKey) {
    PsiIdentifier name = null;
    String message = null;
    if(scope.getParent() instanceof PsiMethod) {
      name = ((PsiMethod)scope.getParent()).getNameIdentifier();
      message = JavaAnalysisBundle.message(problemKey, "Method <code>#ref</code>");
    } else if(scope instanceof PsiClass) {
      name = ((PsiClass)scope).getNameIdentifier();
      message = JavaAnalysisBundle.message(problemKey, "Class initializer");
    }
    if (name != null) { // Might be null for synthetic methods like JSP page.
      holder.registerProblem(name, message, ProblemHighlightType.WEAK_WARNING);
    }
  }

  protected @NotNull List<@NotNull LocalQuickFix> createCastFixes(PsiTypeCastExpression castExpression,
                                                                  PsiType realType,
                                                                  boolean onTheFly,
                                                                  boolean alwaysFails) {
    return Collections.emptyList();
  }

  protected @NotNull List<@NotNull LocalQuickFix> createNPEFixes(@Nullable PsiExpression qualifier,
                                                                 PsiExpression expression,
                                                                 boolean onTheFly,
                                                                 boolean alwaysNull) {
    return Collections.emptyList();
  }

  protected @NotNull List<@NotNull LocalQuickFix> createUnboxingNullableFixes(@NotNull PsiExpression qualifier, PsiElement anchor, boolean onTheFly) {
    return Collections.emptyList();
  }

  protected @NotNull List<@NotNull LocalQuickFix> createMethodReferenceNPEFixes(PsiMethodReferenceExpression methodRef, boolean onTheFly) {
    return Collections.emptyList();
  }

  protected @Nullable LocalQuickFix createUnwrapSwitchLabelFix() {
    return null;
  }

  protected @Nullable LocalQuickFix createIntroduceVariableFix() {
    return null;
  }

  protected LocalQuickFix createRemoveAssignmentFix(PsiAssignmentExpression assignment) {
    return null;
  }

  private void createDescription(ProblemsHolder holder,
                                 final DataFlowInstructionVisitor visitor,
                                 PsiElement scope,
                                 Instruction @NotNull [] instructions) {
    ProblemReporter reporter = new ProblemReporter(holder, scope);

    reportFailingCasts(reporter, visitor);
    reportUnreachableSwitchBranches(visitor.getSwitchLabelsReachability(), holder);

    reportAlwaysFailingCalls(reporter, visitor);

    List<NullabilityProblem<?>> problems = NullabilityProblemKind.postprocessNullabilityProblems(visitor.problems().toList());
    reportNullabilityProblems(reporter, problems);
    reportNullableReturns(reporter, problems, scope);

    reportRedundantInstanceOf(visitor, reporter);

    reportArrayAccessProblems(holder, visitor);

    reportArrayStoreProblems(holder, visitor);

    if (REPORT_NULLABLE_METHODS_RETURNING_NOT_NULL && visitor.isAlwaysReturnsNotNull(instructions)) {
      reportAlwaysReturnsNotNull(holder, scope);
    }

    reportMutabilityViolations(holder, visitor.getMutabilityViolations(true),
                               JavaAnalysisBundle.message("dataflow.message.immutable.modified"));
    reportMutabilityViolations(holder, visitor.getMutabilityViolations(false),
                               JavaAnalysisBundle.message("dataflow.message.immutable.passed"));

    reportDuplicateAssignments(reporter, visitor);
    reportPointlessSameArguments(reporter, visitor);
    reportStreamConsumed(holder, visitor);
  }

  private static void reportRedundantInstanceOf(DataFlowInstructionVisitor visitor, ProblemReporter reporter) {
    visitor.redundantInstanceOfs().forEach(anchor -> {
      PsiExpression expression =
        anchor instanceof JavaExpressionAnchor ? ((JavaExpressionAnchor)anchor).getExpression() :
        anchor instanceof JavaMethodReferenceReturnAnchor ? ((JavaMethodReferenceReturnAnchor)anchor).getMethodReferenceExpression() :
        null;
      if (expression == null || shouldBeSuppressed(expression)) return;
      if (ContainerUtil.exists(JavaPsiPatternUtil.getExposedPatternVariables(expression),
                               var -> VariableAccessUtils.variableIsUsed(var, var.getDeclarationScope()))) {
        return;
      }
      ModCommandAction action = new RedundantInstanceofFix(expression);
      reporter.registerProblem(expression,
                               JavaAnalysisBundle.message("dataflow.message.redundant.instanceof"),
                               LocalQuickFix.from(action));
    });
  }

  private void reportUnreachableSwitchBranches(Map<PsiCaseLabelElement, ThreeState> labelReachability, ProblemsHolder holder) {
    if (labelReachability.isEmpty()) return;
    Set<PsiSwitchBlock> coveredSwitches = new HashSet<>();
    Map<PsiCaseLabelElement, PsiSwitchBlock> unreachableLabels = new HashMap<>();

    for (Map.Entry<PsiCaseLabelElement, ThreeState> entry : labelReachability.entrySet()) {
      if (entry.getValue() != ThreeState.YES) continue;
      PsiCaseLabelElement label = entry.getKey();
      PsiSwitchLabelStatementBase labelStatement = PsiImplUtil.getSwitchLabel(label);
      if (labelStatement == null) continue; // could be a guard
      PsiExpression guardExpression = labelStatement.getGuardExpression();
      if (guardExpression != null) {
        ThreeState guardReachability = labelReachability.get(guardExpression);
        if (guardReachability != ThreeState.YES) continue;
      }
      PsiSwitchBlock switchBlock = labelStatement.getEnclosingSwitchBlock();
      if (switchBlock == null) continue;
      if (!canRemoveTheOnlyReachableLabel(label, switchBlock)) continue;
      if (!StreamEx.iterate(labelStatement, Objects::nonNull, l -> PsiTreeUtil.getPrevSiblingOfType(l, PsiSwitchLabelStatementBase.class))
        .skip(1).map(PsiSwitchLabelStatementBase::getCaseLabelElementList)
        .nonNull().flatArray(PsiCaseLabelElementList::getElements)
        .append(StreamEx.iterate(label, Objects::nonNull, l -> PsiTreeUtil.getPrevSiblingOfType(l, PsiCaseLabelElement.class)).skip(1))
        .allMatch(l -> labelReachability.get(l) == ThreeState.NO)) {

        // Add all labels after always-reachable one as unreachable
        StreamEx.iterate(labelStatement, Objects::nonNull, l -> PsiTreeUtil.getNextSiblingOfType(l, PsiSwitchLabelStatementBase.class))
          .remove(SwitchUtils::isDefaultLabel)
          .skip(1).map(PsiSwitchLabelStatementBase::getCaseLabelElementList)
          .nonNull().flatArray(PsiCaseLabelElementList::getElements)
          .append(StreamEx.iterate(label, Objects::nonNull, l -> PsiTreeUtil.getNextSiblingOfType(l, PsiCaseLabelElement.class)).skip(1))
          .forEach(l -> unreachableLabels.put(l, switchBlock));
        continue;
      }
      coveredSwitches.add(switchBlock);
      LocalQuickFix unwrapFix;
      if ((switchBlock instanceof PsiSwitchExpression && !CodeBlockSurrounder.canSurround(((PsiSwitchExpression)switchBlock))) ||
          SwitchUtils.findRemovableUnreachableBranches(label, switchBlock).isEmpty()) {
        unwrapFix = null;
      }
      else {
        unwrapFix = createUnwrapSwitchLabelFix();
      }
      holder.registerProblem(label, JavaAnalysisBundle.message("dataflow.message.only.switch.label"),
                             LocalQuickFix.notNullElements(unwrapFix));
    }

    for (Map.Entry<PsiCaseLabelElement, ThreeState> entry : labelReachability.entrySet()) {
      if (entry.getValue() != ThreeState.NO) continue;
      PsiCaseLabelElement label = entry.getKey();
      PsiSwitchLabelStatementBase labelStatement = PsiImplUtil.getSwitchLabel(label);
      if (labelStatement == null) continue;
      PsiSwitchBlock switchBlock = labelStatement.getEnclosingSwitchBlock();
      if (switchBlock == null || coveredSwitches.contains(switchBlock)) continue;
      unreachableLabels.put(label, switchBlock);
    }
    unreachableLabels.forEach((label, switchBlock) -> {
      if (isThrowing(label)) return;
      // duplicate case label is a compilation error so no need to highlight by the inspection
      Set<PsiElement> suspiciousElements = findSuspiciousLabelElements(switchBlock);
      if (!suspiciousElements.contains(label)) {
        holder.problem(label, JavaAnalysisBundle.message("dataflow.message.unreachable.switch.label"))
            .maybeFix(createDeleteLabelFix(label)).register();
      }
    });
  }

  protected @Nullable LocalQuickFix createDeleteLabelFix(PsiCaseLabelElement label) {
    return null;
  }

  private static boolean isThrowing(PsiCaseLabelElement label) {
    PsiCaseLabelElementList caseLabelList = tryCast(label.getParent(), PsiCaseLabelElementList.class);
    if (caseLabelList == null) return false;
    PsiSwitchLabelStatementBase labelStatement = tryCast(caseLabelList.getParent(), PsiSwitchLabelStatementBase.class);
    if (labelStatement == null) return false;
    if (labelStatement instanceof PsiSwitchLabeledRuleStatement) {
      return ControlFlowUtils.stripBraces(((PsiSwitchLabeledRuleStatement)labelStatement).getBody()) instanceof PsiThrowStatement;
    }
    if (labelStatement instanceof PsiSwitchLabelStatement) {
      PsiElement cur = labelStatement;
      while(true) {
        PsiElement next = cur.getNextSibling();
        if (!(next instanceof PsiComment) && !(next instanceof PsiWhiteSpace) && !(next instanceof PsiSwitchLabelStatement)) {
          return next instanceof PsiThrowStatement;
        }
        cur = next;
      }
    }
    return false;
  }

  private static boolean canRemoveTheOnlyReachableLabel(@NotNull PsiCaseLabelElement label, @NotNull PsiSwitchBlock switchBlock) {
    if (!(label instanceof PsiPattern)) return true;
    PsiExpression selector = switchBlock.getExpression();
    if (selector == null) return false;
    PsiType selectorType = selector.getType();
    if (selectorType == null) return false;
    if (!JavaPsiPatternUtil.isUnconditionalForType(label, selectorType)) return true;
    int branchCount = SwitchUtils.calculateBranchCount(switchBlock);
    // it's a compilation error if switch contains both default and an unconditional pattern, so no additional suggestion is needed
    return branchCount > 1;
  }

  private static void reportPointlessSameArguments(ProblemReporter reporter, DataFlowInstructionVisitor visitor) {
    visitor.pointlessSameArguments().forKeyValue((expr, eq) -> {
      PsiElement name = expr.getReferenceNameElement();
      if (name != null) {
        PsiExpression[] expressions = PsiExpression.EMPTY_ARRAY;
        if (expr.getParent() instanceof PsiMethodCallExpression) {
          expressions = ((PsiMethodCallExpression)expr.getParent()).getArgumentList().getExpressions();
          if (expressions.length == 2 && PsiUtil.isConstantExpression(expressions[0]) && PsiUtil.isConstantExpression(expressions[1]) &&
              !EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(expressions[0], expressions[1])) {
            return;
          }
        }
        if (eq.firstArgEqualToResult()) {
          String message = eq.argsEqual() ? JavaAnalysisBundle.message("dataflow.message.pointless.same.arguments") :
                           JavaAnalysisBundle.message("dataflow.message.pointless.same.argument.and.result", 1);
          LocalQuickFix fix = expressions.length == 2 ? new ReplaceWithArgumentFix(expressions[0], 0) : null;
          reporter.registerProblem(name, message, LocalQuickFix.notNullElements(fix));
        }
        else if (eq.argsEqual()) {
          reporter.registerProblem(name, JavaAnalysisBundle.message("dataflow.message.pointless.same.arguments"));
        }
        else if (eq.secondArgEqualToResult()) {
          LocalQuickFix fix = expressions.length == 2 ? new ReplaceWithArgumentFix(expressions[1], 1) : null;
          reporter.registerProblem(name, JavaAnalysisBundle.message("dataflow.message.pointless.same.argument.and.result", 2),
                                   LocalQuickFix.notNullElements(fix));
        }
      }
    });
  }

  private void reportDuplicateAssignments(ProblemReporter reporter, DataFlowInstructionVisitor visitor) {
    visitor.sameValueAssignments().forEach(expr -> {
      expr = PsiUtil.skipParenthesizedExprDown(expr);
      if (expr == null) return;
      PsiAssignmentExpression assignment = PsiTreeUtil.getParentOfType(expr, PsiAssignmentExpression.class);
      PsiElement context = PsiTreeUtil.getParentOfType(expr, PsiForStatement.class, PsiClassInitializer.class);
      if (context instanceof PsiForStatement && PsiTreeUtil.isAncestor(((PsiForStatement)context).getInitialization(), expr, true)) {
        return;
      }
      if (expr instanceof PsiReferenceExpression ref && ref.resolve() instanceof PsiField field) {
        // Final field assignment: even if redundant according to DFA model (e.g. this.field = null),
        // it's necessary due to language semantics
        if (field.hasModifierProperty(PsiModifier.FINAL)) return;
        if (context instanceof PsiClassInitializer) {
          if (assignment != null) {
            Object constValue = ExpressionUtils.computeConstantExpression(assignment.getRExpression());
            if (constValue == PsiTypesUtil.getDefaultValue(expr.getType())) {
              if ((field.hasModifierProperty(PsiModifier.STATIC) || ExpressionUtil.isEffectivelyUnqualified(ref)) &&
                  field.getContainingClass() == ((PsiClassInitializer)context).getContainingClass()) {
                return;
              }
            }
          }
        }
      }
      DfType value = CommonDataflow.getDfType(expr, IGNORE_ASSERT_STATEMENTS);
      // reported by ConstantValueInspection
      if (value == DfTypes.TRUE || value == DfTypes.FALSE) return;
      String message = assignment != null && !assignment.getOperationTokenType().equals(JavaTokenType.EQ)
                       ? JavaAnalysisBundle.message("dataflow.message.redundant.update")
                       : JavaAnalysisBundle.message("dataflow.message.redundant.assignment");
      reporter.registerProblem(expr, message, LocalQuickFix.notNullElements(createRemoveAssignmentFix(assignment)));
    });
  }

  private void reportMutabilityViolations(ProblemsHolder holder, Set<PsiElement> violations, @InspectionMessage String message) {
    for (PsiElement violation : violations) {
      holder.registerProblem(violation, message, LocalQuickFix.notNullElements(createMutabilityViolationFix(violation)));
    }
  }

  protected LocalQuickFix createMutabilityViolationFix(PsiElement violation) {
    return null;
  }

  protected void reportNullabilityProblems(ProblemReporter reporter, List<NullabilityProblem<?>> problems) {
    for (NullabilityProblem<?> problem : problems) {
      PsiExpression expression = problem.getDereferencedExpression();
      boolean nullLiteral = ExpressionUtils.isNullLiteral(expression);
      if (!REPORT_UNSOUND_WARNINGS) {
        if (expression == null || !nullLiteral && CommonDataflow.getDfType(expression, IGNORE_ASSERT_STATEMENTS) != DfTypes.NULL) continue;
      }
      // Expression of null type: could be failed LVTI, skip it to avoid confusion
      if (expression != null && !nullLiteral && PsiTypes.nullType().equals(expression.getType())) continue;
      boolean alwaysNull = problem.isAlwaysNull(IGNORE_ASSERT_STATEMENTS);
      NullabilityProblemKind.innerClassNPE.ifMyProblem(problem, newExpression -> {
        List<LocalQuickFix> fixes = createNPEFixes(newExpression.getQualifier(), newExpression, reporter.isOnTheFly(), alwaysNull);
        reporter
          .registerProblem(getElementToHighlight(newExpression), problem.getMessage(IGNORE_ASSERT_STATEMENTS),
                           fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
      });
      NullabilityProblemKind.callMethodRefNPE.ifMyProblem(problem, methodRef ->
        reporter.registerProblem(methodRef, JavaAnalysisBundle.message("dataflow.message.npe.methodref.invocation"),
                                 createMethodReferenceNPEFixes(methodRef, reporter.isOnTheFly()).toArray(LocalQuickFix.EMPTY_ARRAY)));
      NullabilityProblemKind.callNPE.ifMyProblem(problem, call ->
        reportCallMayProduceNpe(reporter, problem.getMessage(IGNORE_ASSERT_STATEMENTS), call, alwaysNull));
      NullabilityProblemKind.passingToNotNullParameter.ifMyProblem(problem, expr -> {
        List<LocalQuickFix> fixes = createNPEFixes(expression, expression, reporter.isOnTheFly(), alwaysNull);
        reporter.registerProblem(expression, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
      });
      NullabilityProblemKind.passingToNotNullMethodRefParameter.ifMyProblem(problem, methodRef -> {
        LocalQuickFix[] fixes = createMethodReferenceNPEFixes(methodRef, reporter.isOnTheFly()).toArray(LocalQuickFix.EMPTY_ARRAY);
        reporter.registerProblem(methodRef, JavaAnalysisBundle.message("dataflow.message.passing.nullable.argument.methodref"), fixes);
      });
      NullabilityProblemKind.unboxingMethodRefParameter.ifMyProblem(problem, methodRef -> {
        LocalQuickFix[] fixes = createMethodReferenceNPEFixes(methodRef, reporter.isOnTheFly()).toArray(LocalQuickFix.EMPTY_ARRAY);
        reporter.registerProblem(methodRef, JavaAnalysisBundle.message("dataflow.message.unboxing.nullable.argument.methodref"), fixes);
      });
      NullabilityProblemKind.arrayAccessNPE.ifMyProblem(problem, arrayAccess -> {
        LocalQuickFix[] fixes = createNPEFixes(arrayAccess.getArrayExpression(), arrayAccess, reporter.isOnTheFly(),
                                               alwaysNull).toArray(LocalQuickFix.EMPTY_ARRAY);
        reporter.registerProblem(arrayAccess, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fixes);
      });
      NullabilityProblemKind.templateNPE.ifMyProblem(problem, template -> {
        PsiExpression processor = template.getProcessor();
        LocalQuickFix[] fixes = createNPEFixes(processor, template, reporter.isOnTheFly(),
                                               alwaysNull).toArray(LocalQuickFix.EMPTY_ARRAY);
        reporter.registerProblem(processor, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fixes);
      });
      NullabilityProblemKind.fieldAccessNPE.ifMyProblem(problem, element -> {
        PsiElement parent = element.getParent();
        PsiExpression fieldAccess = parent instanceof PsiReferenceExpression ? (PsiExpression)parent : element;
        LocalQuickFix[] fix = createNPEFixes(element, fieldAccess, reporter.isOnTheFly(), alwaysNull).toArray(LocalQuickFix.EMPTY_ARRAY);
        reporter.registerProblem(element, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fix);
      });
      NullabilityProblemKind.unboxingNullable.ifMyProblem(problem, element -> {
        PsiExpression anchor = expression;
        if (anchor instanceof PsiTypeCastExpression && anchor.getType() instanceof PsiPrimitiveType) {
          anchor = Objects.requireNonNull(((PsiTypeCastExpression)anchor).getOperand());
        }
        if (anchor != null) {
          LocalQuickFix[] fixes = createUnboxingNullableFixes(anchor, element, reporter.isOnTheFly()).toArray(LocalQuickFix.EMPTY_ARRAY);
          reporter.registerProblem(anchor, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fixes);
        }
      });
      NullabilityProblemKind.nullableFunctionReturn.ifMyProblem(
        problem, expr -> reporter.registerProblem(expression == null ? expr : expression, problem.getMessage(IGNORE_ASSERT_STATEMENTS)));
      Consumer<PsiExpression> reportNullability = expr -> reportNullabilityProblem(reporter, problem, expression);
      NullabilityProblemKind.assigningToNotNull.ifMyProblem(problem, reportNullability);
      NullabilityProblemKind.storingToNotNullArray.ifMyProblem(problem, reportNullability);
      if (SUGGEST_NULLABLE_ANNOTATIONS) {
        NullabilityProblemKind.passingToNonAnnotatedMethodRefParameter.ifMyProblem(
          problem, methodRef -> reportNullableArgumentPassedToNonAnnotatedMethodRef(reporter, problem, methodRef));
        NullabilityProblemKind.passingToNonAnnotatedParameter.ifMyProblem(
          problem,
          top -> reportNullableArgumentsPassedToNonAnnotated(reporter, problem.getMessage(IGNORE_ASSERT_STATEMENTS), expression, top,
                                                             alwaysNull));
        NullabilityProblemKind.assigningToNonAnnotatedField.ifMyProblem(
          problem, top -> reportNullableAssignedToNonAnnotatedField(reporter, top, expression, problem.getMessage(IGNORE_ASSERT_STATEMENTS),
                                                                    alwaysNull));
      }
    }
  }

  private void reportNullabilityProblem(ProblemReporter reporter,
                                        NullabilityProblem<?> problem,
                                        PsiExpression expr) {
    LocalQuickFix[] fixes = createNPEFixes(expr, expr, reporter.isOnTheFly(), problem.isAlwaysNull(IGNORE_ASSERT_STATEMENTS))
      .toArray(LocalQuickFix.EMPTY_ARRAY);
    reporter.registerProblem(expr, problem.getMessage(IGNORE_ASSERT_STATEMENTS), fixes);
  }

  private static void reportArrayAccessProblems(ProblemsHolder holder, DataFlowInstructionVisitor visitor) {
    visitor.outOfBoundsArrayAccesses().forEach(access -> {
      PsiExpression indexExpression = access.getIndexExpression();
      if (indexExpression != null) {
        holder.registerProblem(indexExpression, JavaAnalysisBundle.message("dataflow.message.array.index.out.of.bounds"));
      }
    });
    visitor.negativeArraySizes().forEach(dimExpression -> {
      holder.registerProblem(dimExpression, JavaAnalysisBundle.message("dataflow.message.negative.array.size"));
    });
  }

  private void reportStreamConsumed(ProblemsHolder holder, DataFlowInstructionVisitor visitor) {
    visitor.streamConsumed().forKeyValue((psiElement, alwaysFails) -> {
      if (!REPORT_UNSOUND_WARNINGS && !alwaysFails) return;
      holder.registerProblem(psiElement, JavaAnalysisBundle.message(alwaysFails ? "dataflow.message.stream.consumed.always" :
                                                                    "dataflow.message.stream.consumed"));
    });
  }

  private static void reportArrayStoreProblems(ProblemsHolder holder, DataFlowInstructionVisitor visitor) {
    visitor.getArrayStoreProblems().forEach(
      (assignment, types) -> holder.registerProblem(assignment.getOperationSign(), JavaAnalysisBundle
        .message("dataflow.message.arraystore", types.getFirst().getCanonicalText(), types.getSecond().getCanonicalText())));
  }

