@NotNull
  private List<PreContract> visitExpression(final List<ValueConstraint[]> states, @Nullable LighterASTNode expr) {
    if (expr == null) return emptyList();
    if (states.isEmpty()) return emptyList();
    if (states.size() > 300) return emptyList(); // too complex

    IElementType type = expr.getTokenType();
    if (type == POLYADIC_EXPRESSION || type == BINARY_EXPRESSION) {
      return visitPolyadic(states, expr);
    }

    if (type == CONDITIONAL_EXPRESSION) {
      List<LighterASTNode> children = getExpressionChildren(myTree, expr);
      if (children.size() != 3) return emptyList();

      List<PreContract> conditionResults = visitExpression(states, children.get(0));
      return ContainerUtil.concat(
        visitExpression(antecedentsReturning(conditionResults, returnTrue()), children.get(1)),
        visitExpression(antecedentsReturning(conditionResults, returnFalse()), children.get(2)));
    }


    if (type == PARENTH_EXPRESSION) {
      return visitExpression(states, findExpressionChild(myTree, expr));
    }
    if (type == TYPE_CAST_EXPRESSION && !isPrimitiveCast(myTree, expr)) {
      return visitExpression(states, findExpressionChild(myTree, expr));
    }

    if (isNegationExpression(expr)) {
      return ContainerUtil.mapNotNull(visitExpression(states, findExpressionChild(myTree, expr)), PreContract::negate);
    }

    if (type == INSTANCE_OF_EXPRESSION) {
      final int parameter = resolveParameter(findExpressionChild(myTree, expr));
      if (parameter >= 0) {
        return asPreContracts(ContainerUtil.mapNotNull(states, state -> contractWithConstraint(state, parameter, NULL_VALUE, returnFalse())));
      }
    }

    if (type == NEW_EXPRESSION) {
      return asPreContracts(toContracts(states, returnNew()));
    }
    if (type == THIS_EXPRESSION) {
      return asPreContracts(toContracts(states, returnThis()));
    }
    if (type == METHOD_CALL_EXPRESSION) {
      return singletonList(new MethodCallContract(ExpressionRange.create(expr, myBody.getStartOffset()),
                                                              ContainerUtil.map(states, Arrays::asList)));
    }

    final ValueConstraint constraint = getLiteralConstraint(expr);
    if (constraint != null) {
      return asPreContracts(toContracts(states, constraint.asReturnValue()));
    }

    int paramIndex = resolveParameter(expr);
    if (paramIndex >= 0) {
      List<StandardMethodContract> result = new ArrayList<>();
      for (ValueConstraint[] state : states) {
        if (state[paramIndex] == TRUE_VALUE || state[paramIndex] == FALSE_VALUE || state[paramIndex] == NULL_VALUE) {
          // like "if(x == null) return x": no need to refer to parameter
          result.add(new StandardMethodContract(state, state[paramIndex].asReturnValue()));
        } else if (JavaTokenType.BOOLEAN_KEYWORD == getPrimitiveParameterType(paramIndex)) {
          // if (boolValue) ...
          ContainerUtil.addIfNotNull(result, contractWithConstraint(state, paramIndex, TRUE_VALUE, returnTrue()));
          ContainerUtil.addIfNotNull(result, contractWithConstraint(state, paramIndex, FALSE_VALUE, returnFalse()));
        } else {
          result.add(new StandardMethodContract(state, returnParameter(paramIndex)));
        }
      }
      return asPreContracts(result);
    }

    return emptyList();
  }
@NotNull
  private List<PreContract> visitExpression(final List<ValueConstraint[]> states, @Nullable LighterASTNode expr)