@Nullable
  static BaseStreamApiMigration findMigration(PsiStatement loop,
                                              PsiElement body,
                                              TerminalBlock tb,
                                              boolean suggestForeach,
                                              boolean replaceTrivialForEach) {
    final ControlFlow controlFlow;
    try {
      controlFlow = ControlFlowFactory.getInstance(loop.getProject())
        .getControlFlow(body, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance());
    }
    catch (AnalysisCanceledException ignored) {
      return null;
    }
    int startOffset = controlFlow.getStartOffset(body);
    int endOffset = controlFlow.getEndOffset(body);
    if (startOffset < 0 || endOffset < 0) return null;
    PsiElement surrounder = PsiTreeUtil.getParentOfType(loop, PsiLambdaExpression.class, PsiClass.class);
    final List<PsiVariable> nonFinalVariables = StreamEx.of(ControlFlowUtil.getUsedVariables(controlFlow, startOffset, endOffset))
      .remove(variable -> variable instanceof PsiField)
      .remove(variable -> PsiTreeUtil.getParentOfType(variable, PsiLambdaExpression.class, PsiClass.class) != surrounder)
      .remove(variable -> isVariableSuitableForStream(variable, loop, tb)).toList();

    if (isCountOperation(nonFinalVariables, tb)) {
      return new CountMigration(true);
    }
    CollectMigration.CollectTerminal terminal = CollectMigration.extractCollectTerminal(tb, nonFinalVariables);
    if (terminal != null) {
      boolean addAll = loop instanceof PsiForeachStatement && !tb.hasOperations() && isAddAllCall(tb);
      // Don't suggest to convert the loop which can be trivially replaced via addAll:
      // this is covered by UseBulkOperationInspection and ManualArrayToCollectionCopyInspection
      if (addAll) return null;
      boolean shouldWarn = replaceTrivialForEach ||
                           tb.hasOperations() ||
                           tb.getLastOperation() instanceof BufferedReaderLines ||
                           !terminal.isTrivial();
      return new CollectMigration(shouldWarn, terminal.getMethodName());
    }
    if (JoiningMigration.extractTerminal(tb, nonFinalVariables) != null) {
      return new JoiningMigration(true);
    }
    if (tb.getCountExpression() != null || tb.isEmpty()) return null;
    if (nonFinalVariables.isEmpty() && extractArray(tb) != null) {
      return new ToArrayMigration(true);
    }
    if (getAccumulatedVariable(tb, nonFinalVariables, SUM_OPERATION) != null) {
      return new SumMigration(true);
    }
    FindExtremumMigration.ExtremumTerminal extremumTerminal = FindExtremumMigration.extract(tb, nonFinalVariables);
    if (extremumTerminal != null) {
      return new FindExtremumMigration(true, FindExtremumMigration.getOperation(extremumTerminal.isMax()));
    }
    for (OperationReductionMigration.ReductionOperation reductionOperation : OperationReductionMigration.OPERATIONS) {
      if (getAccumulatedVariable(tb, nonFinalVariables, reductionOperation) != null) {
        return new OperationReductionMigration(true, reductionOperation);
      }
    }
    Collection<PsiStatement> exitPoints = tb.findExitPoints(controlFlow);
    if (exitPoints == null) return null;
    boolean onlyNonLabeledContinue = ContainerUtil.and(
      exitPoints, statement -> statement instanceof PsiContinueStatement && ((PsiContinueStatement)statement).getLabelIdentifier() == null);
    if (onlyNonLabeledContinue && nonFinalVariables.isEmpty()) {
      boolean shouldWarn = suggestForeach &&
                           (replaceTrivialForEach ||
                            tb.hasOperations() ||
                            ForEachMigration.tryExtractMapExpression(tb) != null ||
                            !isTrivial(tb));
      return new ForEachMigration(shouldWarn, "forEach");
    }
    if (nonFinalVariables.isEmpty() && tb.getSingleStatement() instanceof PsiReturnStatement) {
      return findMigrationForReturn(loop, tb, replaceTrivialForEach);
    }
    // Source and intermediate ops should not refer to non-final variables
    if (tb.intermediateAndSourceExpressions()
      .flatCollection(expr -> PsiTreeUtil.collectElementsOfType(expr, PsiReferenceExpression.class))
      .map(PsiReferenceExpression::resolve).select(PsiVariable.class).anyMatch(nonFinalVariables::contains)) {
      return null;
    }
    PsiStatement[] statements = tb.getStatements();
    if (statements.length == 2) {
      PsiStatement breakStatement = statements[1];
      if (loop instanceof PsiLoopStatement && ControlFlowUtils.statementBreaksLoop(breakStatement, (PsiLoopStatement)loop) &&
          exitPoints.size() == 1 &&
          exitPoints.contains(breakStatement)) {
        return findMigrationForBreak(tb, nonFinalVariables, statements[0], replaceTrivialForEach);
      }
    }
    return null;
  }
@Nullable
  static BaseStreamApiMigration findMigration(PsiStatement loop,
                                              PsiElement body,
                                              TerminalBlock tb,
                                              boolean suggestForeach,
                                              boolean replaceTrivialForEach)