//===-- SimplifyBooleanExprCheck.cpp - clang-tidy -------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "SimplifyBooleanExprCheck.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Lex/Lexer.h"
#include "llvm/Support/SaveAndRestore.h"

#include <optional>
#include <string>
#include <utility>

using namespace clang::ast_matchers;

namespace clang::tidy::readability {

namespace {

StringRef getText(const ASTContext &Context, SourceRange Range) {
  return Lexer::getSourceText(CharSourceRange::getTokenRange(Range),
                              Context.getSourceManager(),
                              Context.getLangOpts());
}

template <typename T> StringRef getText(const ASTContext &Context, T &Node) {
  return getText(Context, Node.getSourceRange());
}

} // namespace

static constexpr char SimplifyOperatorDiagnostic[] =
    "redundant boolean literal supplied to boolean operator";
static constexpr char SimplifyConditionDiagnostic[] =
    "redundant boolean literal in if statement condition";
static constexpr char SimplifyConditionalReturnDiagnostic[] =
    "redundant boolean literal in conditional return statement";

static bool needsParensAfterUnaryNegation(const Expr *E) {
  E = E->IgnoreImpCasts();
  if (isa<BinaryOperator>(E) || isa<ConditionalOperator>(E))
    return true;

  if (const auto *Op = dyn_cast<CXXOperatorCallExpr>(E))
    return Op->getNumArgs() == 2 && Op->getOperator() != OO_Call &&
           Op->getOperator() != OO_Subscript;

  return false;
}

static std::pair<BinaryOperatorKind, BinaryOperatorKind> Opposites[] = {
    {BO_LT, BO_GE}, {BO_GT, BO_LE}, {BO_EQ, BO_NE}};

static StringRef negatedOperator(const BinaryOperator *BinOp) {
  const BinaryOperatorKind Opcode = BinOp->getOpcode();
  for (auto NegatableOp : Opposites) {
    if (Opcode == NegatableOp.first)
      return BinOp->getOpcodeStr(NegatableOp.second);
    if (Opcode == NegatableOp.second)
      return BinOp->getOpcodeStr(NegatableOp.first);
  }
  return {};
}

static std::pair<OverloadedOperatorKind, StringRef> OperatorNames[] = {
    {OO_EqualEqual, "=="},   {OO_ExclaimEqual, "!="}, {OO_Less, "<"},
    {OO_GreaterEqual, ">="}, {OO_Greater, ">"},       {OO_LessEqual, "<="}};

static StringRef getOperatorName(OverloadedOperatorKind OpKind) {
  for (auto Name : OperatorNames) {
    if (Name.first == OpKind)
      return Name.second;
  }

  return {};
}

static std::pair<OverloadedOperatorKind, OverloadedOperatorKind>
    OppositeOverloads[] = {{OO_EqualEqual, OO_ExclaimEqual},
                           {OO_Less, OO_GreaterEqual},
                           {OO_Greater, OO_LessEqual}};

static StringRef negatedOperator(const CXXOperatorCallExpr *OpCall) {
  const OverloadedOperatorKind Opcode = OpCall->getOperator();
  for (auto NegatableOp : OppositeOverloads) {
    if (Opcode == NegatableOp.first)
      return getOperatorName(NegatableOp.second);
    if (Opcode == NegatableOp.second)
      return getOperatorName(NegatableOp.first);
  }
  return {};
}

static std::string asBool(StringRef Text, bool NeedsStaticCast) {
  if (NeedsStaticCast)
    return ("static_cast<bool>(" + Text + ")").str();

  return std::string(Text);
}

static bool needsNullPtrComparison(const Expr *E) {
  if (const auto *ImpCast = dyn_cast<ImplicitCastExpr>(E))
    return ImpCast->getCastKind() == CK_PointerToBoolean ||
           ImpCast->getCastKind() == CK_MemberPointerToBoolean;

  return false;
}

static bool needsZeroComparison(const Expr *E) {
  if (const auto *ImpCast = dyn_cast<ImplicitCastExpr>(E))
    return ImpCast->getCastKind() == CK_IntegralToBoolean;

  return false;
}

static bool needsStaticCast(const Expr *E) {
  if (const auto *ImpCast = dyn_cast<ImplicitCastExpr>(E)) {
    if (ImpCast->getCastKind() == CK_UserDefinedConversion &&
        ImpCast->getSubExpr()->getType()->isBooleanType()) {
      if (const auto *MemCall =
              dyn_cast<CXXMemberCallExpr>(ImpCast->getSubExpr())) {
        if (const auto *MemDecl =
                dyn_cast<CXXConversionDecl>(MemCall->getMethodDecl())) {
          if (MemDecl->isExplicit())
            return true;
        }
      }
    }
  }

  E = E->IgnoreImpCasts();
  return !E->getType()->isBooleanType();
}

static std::string compareExpressionToConstant(const ASTContext &Context,
                                               const Expr *E, bool Negated,
                                               const char *Constant) {
  E = E->IgnoreImpCasts();
  const std::string ExprText =
      (isa<BinaryOperator>(E) ? ("(" + getText(Context, *E) + ")")
                              : getText(Context, *E))
          .str();
  return ExprText + " " + (Negated ? "!=" : "==") + " " + Constant;
}

static std::string compareExpressionToNullPtr(const ASTContext &Context,
                                              const Expr *E, bool Negated) {
  const char *NullPtr = Context.getLangOpts().CPlusPlus11 ? "nullptr" : "NULL";
  return compareExpressionToConstant(Context, E, Negated, NullPtr);
}

static std::string compareExpressionToZero(const ASTContext &Context,
                                           const Expr *E, bool Negated) {
  return compareExpressionToConstant(Context, E, Negated, "0");
}

static std::string replacementExpression(const ASTContext &Context,
                                         bool Negated, const Expr *E) {
  E = E->IgnoreParenBaseCasts();
  if (const auto *EC = dyn_cast<ExprWithCleanups>(E))
    E = EC->getSubExpr();

  const bool NeedsStaticCast = needsStaticCast(E);
  if (Negated) {
    if (const auto *UnOp = dyn_cast<UnaryOperator>(E)) {
      if (UnOp->getOpcode() == UO_LNot) {
        if (needsNullPtrComparison(UnOp->getSubExpr()))
          return compareExpressionToNullPtr(Context, UnOp->getSubExpr(), true);

        if (needsZeroComparison(UnOp->getSubExpr()))
          return compareExpressionToZero(Context, UnOp->getSubExpr(), true);

        return replacementExpression(Context, false, UnOp->getSubExpr());
      }
    }

    if (needsNullPtrComparison(E))
      return compareExpressionToNullPtr(Context, E, false);

    if (needsZeroComparison(E))
      return compareExpressionToZero(Context, E, false);

    StringRef NegatedOperator;
    const Expr *LHS = nullptr;
    const Expr *RHS = nullptr;
    if (const auto *BinOp = dyn_cast<BinaryOperator>(E)) {
      NegatedOperator = negatedOperator(BinOp);
      LHS = BinOp->getLHS();
      RHS = BinOp->getRHS();
    } else if (const auto *OpExpr = dyn_cast<CXXOperatorCallExpr>(E)) {
      if (OpExpr->getNumArgs() == 2) {
        NegatedOperator = negatedOperator(OpExpr);
        LHS = OpExpr->getArg(0);
        RHS = OpExpr->getArg(1);
      }
    }
    if (!NegatedOperator.empty() && LHS && RHS)
      return (asBool((getText(Context, *LHS) + " " + NegatedOperator + " " +
                      getText(Context, *RHS))
                         .str(),
                     NeedsStaticCast));

    StringRef Text = getText(Context, *E);
    if (!NeedsStaticCast && needsParensAfterUnaryNegation(E))
      return ("!(" + Text + ")").str();

    if (needsNullPtrComparison(E))
      return compareExpressionToNullPtr(Context, E, false);

    if (needsZeroComparison(E))
      return compareExpressionToZero(Context, E, false);

    return ("!" + asBool(Text, NeedsStaticCast));
  }

  if (const auto *UnOp = dyn_cast<UnaryOperator>(E)) {
    if (UnOp->getOpcode() == UO_LNot) {
      if (needsNullPtrComparison(UnOp->getSubExpr()))
        return compareExpressionToNullPtr(Context, UnOp->getSubExpr(), false);

      if (needsZeroComparison(UnOp->getSubExpr()))
        return compareExpressionToZero(Context, UnOp->getSubExpr(), false);
    }
  }

  if (needsNullPtrComparison(E))
    return compareExpressionToNullPtr(Context, E, true);

  if (needsZeroComparison(E))
    return compareExpressionToZero(Context, E, true);

  return asBool(getText(Context, *E), NeedsStaticCast);
}

static bool containsDiscardedTokens(const ASTContext &Context,
                                    CharSourceRange CharRange) {
  std::string ReplacementText =
      Lexer::getSourceText(CharRange, Context.getSourceManager(),
                           Context.getLangOpts())
          .str();
  Lexer Lex(CharRange.getBegin(), Context.getLangOpts(), ReplacementText.data(),
            ReplacementText.data(),
            ReplacementText.data() + ReplacementText.size());
  Lex.SetCommentRetentionState(true);

  Token Tok;
  while (!Lex.LexFromRawLexer(Tok)) {
    if (Tok.is(tok::TokenKind::comment) || Tok.is(tok::TokenKind::hash))
      return true;
  }

  return false;
}

class SimplifyBooleanExprCheck::Visitor : public RecursiveASTVisitor<Visitor> {
  using Base = RecursiveASTVisitor<Visitor>;

public:
  Visitor(SimplifyBooleanExprCheck *Check, ASTContext &Context)
      : Check(Check), Context(Context) {}

  bool traverse() { return TraverseAST(Context); }

  static bool shouldIgnore(Stmt *S) {
    switch (S->getStmtClass()) {
    case Stmt::ImplicitCastExprClass:
    case Stmt::MaterializeTemporaryExprClass:
    case Stmt::CXXBindTemporaryExprClass:
      return true;
    default:
      return false;
    }
  }

  bool dataTraverseStmtPre(Stmt *S) {
    if (S && !shouldIgnore(S))
      StmtStack.push_back(S);
    return true;
  }

  bool dataTraverseStmtPost(Stmt *S) {
    if (S && !shouldIgnore(S)) {
      assert(StmtStack.back() == S);
      StmtStack.pop_back();
    }
    return true;
  }

  bool VisitBinaryOperator(const BinaryOperator *Op) const {
    Check->reportBinOp(Context, Op);
    return true;
  }

  // Extracts a bool if an expression is (true|false|!true|!false);
  static std::optional<bool> getAsBoolLiteral(const Expr *E, bool FilterMacro) {
    if (const auto *Bool = dyn_cast<CXXBoolLiteralExpr>(E)) {
      if (FilterMacro && Bool->getBeginLoc().isMacroID())
        return std::nullopt;
      return Bool->getValue();
    }
    if (const auto *UnaryOp = dyn_cast<UnaryOperator>(E)) {
      if (FilterMacro && UnaryOp->getBeginLoc().isMacroID())
        return std::nullopt;
      if (UnaryOp->getOpcode() == UO_LNot)
        if (std::optional<bool> Res = getAsBoolLiteral(
                UnaryOp->getSubExpr()->IgnoreImplicit(), FilterMacro))
          return !*Res;
    }
    return std::nullopt;
  }

  template <typename Node> struct NodeAndBool {
    const Node *Item = nullptr;
    bool Bool = false;

    operator bool() const { return Item != nullptr; }
  };

  using ExprAndBool = NodeAndBool<Expr>;
  using DeclAndBool = NodeAndBool<Decl>;

  /// Detect's return (true|false|!true|!false);
  static ExprAndBool parseReturnLiteralBool(const Stmt *S) {
    const auto *RS = dyn_cast<ReturnStmt>(S);
    if (!RS || !RS->getRetValue())
      return {};
    if (std::optional<bool> Ret =
            getAsBoolLiteral(RS->getRetValue()->IgnoreImplicit(), false)) {
      return {RS->getRetValue(), *Ret};
    }
    return {};
  }

  /// If \p S is not a \c CompoundStmt, applies F on \p S, otherwise if there is
  /// only 1 statement in the \c CompoundStmt, applies F on that single
  /// statement.
  template <typename Functor>
  static auto checkSingleStatement(Stmt *S, Functor F) -> decltype(F(S)) {
    if (auto *CS = dyn_cast<CompoundStmt>(S)) {
      if (CS->size() == 1)
        return F(CS->body_front());
      return {};
    }
    return F(S);
  }

  Stmt *parent() const {
    return StmtStack.size() < 2 ? nullptr : StmtStack[StmtStack.size() - 2];
  }

  bool VisitIfStmt(IfStmt *If) {
    // Skip any if's that have a condition var or an init statement, or are
    // "if consteval" statements.
    if (If->hasInitStorage() || If->hasVarStorage() || If->isConsteval())
      return true;
    /*
     * if (true) ThenStmt(); -> ThenStmt();
     * if (false) ThenStmt(); -> <Empty>;
     * if (false) ThenStmt(); else ElseStmt() -> ElseStmt();
     */
    Expr *Cond = If->getCond()->IgnoreImplicit();
    if (std::optional<bool> Bool = getAsBoolLiteral(Cond, true)) {
      if (*Bool)
        Check->replaceWithThenStatement(Context, If, Cond);
      else
        Check->replaceWithElseStatement(Context, If, Cond);
    }

    if (If->getElse()) {
      /*
       * if (Cond) return true; else return false; -> return Cond;
       * if (Cond) return false; else return true; -> return !Cond;
       */
      if (ExprAndBool ThenReturnBool =
              checkSingleStatement(If->getThen(), parseReturnLiteralBool)) {
        ExprAndBool ElseReturnBool =
            checkSingleStatement(If->getElse(), parseReturnLiteralBool);
        if (ElseReturnBool && ThenReturnBool.Bool != ElseReturnBool.Bool) {
          if (Check->ChainedConditionalReturn ||
              !isa_and_nonnull<IfStmt>(parent())) {
            Check->replaceWithReturnCondition(Context, If, ThenReturnBool.Item,
                                              ElseReturnBool.Bool);
          }
        }
      } else {
        /*
         * if (Cond) A = true; else A = false; -> A = Cond;
         * if (Cond) A = false; else A = true; -> A = !Cond;
         */
        Expr *Var = nullptr;
        SourceLocation Loc;
        auto VarBoolAssignmentMatcher = [&Var,
                                         &Loc](const Stmt *S) -> DeclAndBool {
          const auto *BO = dyn_cast<BinaryOperator>(S);
          if (!BO || BO->getOpcode() != BO_Assign)
            return {};
          std::optional<bool> RightasBool =
              getAsBoolLiteral(BO->getRHS()->IgnoreImplicit(), false);
          if (!RightasBool)
            return {};
          Expr *IgnImp = BO->getLHS()->IgnoreImplicit();
          if (!Var) {
            // We only need to track these for the Then branch.
            Loc = BO->getRHS()->getBeginLoc();
            Var = IgnImp;
          }
          if (auto *DRE = dyn_cast<DeclRefExpr>(IgnImp))
            return {DRE->getDecl(), *RightasBool};
          if (auto *ME = dyn_cast<MemberExpr>(IgnImp))
            return {ME->getMemberDecl(), *RightasBool};
          return {};
        };
        if (DeclAndBool ThenAssignment =
                checkSingleStatement(If->getThen(), VarBoolAssignmentMatcher)) {
          DeclAndBool ElseAssignment =
              checkSingleStatement(If->getElse(), VarBoolAssignmentMatcher);
          if (ElseAssignment.Item == ThenAssignment.Item &&
              ElseAssignment.Bool != ThenAssignment.Bool) {
            if (Check->ChainedConditionalAssignment ||
                !isa_and_nonnull<IfStmt>(parent())) {
              Check->replaceWithAssignment(Context, If, Var, Loc,
                                           ElseAssignment.Bool);
            }
          }
        }
      }
    }
    return true;
  }

  bool VisitConditionalOperator(ConditionalOperator *Cond) {
    /*
     * Condition ? true : false; -> Condition
     * Condition ? false : true; -> !Condition;
     */
    if (std::optional<bool> Then =
            getAsBoolLiteral(Cond->getTrueExpr()->IgnoreImplicit(), false)) {
      if (std::optional<bool> Else =
              getAsBoolLiteral(Cond->getFalseExpr()->IgnoreImplicit(), false)) {
        if (*Then != *Else)
          Check->replaceWithCondition(Context, Cond, *Else);
      }
    }
    return true;
  }

  bool VisitCompoundStmt(CompoundStmt *CS) {
    if (CS->size() < 2)
      return true;
    bool CurIf = false, PrevIf = false;
    for (auto First = CS->body_begin(), Second = std::next(First),
              End = CS->body_end();
         Second != End; ++Second, ++First) {
      PrevIf = CurIf;
      CurIf = isa<IfStmt>(*First);
      ExprAndBool TrailingReturnBool = parseReturnLiteralBool(*Second);
      if (!TrailingReturnBool)
        continue;

      if (CurIf) {
        /*
         * if (Cond) return true; return false; -> return Cond;
         * if (Cond) return false; return true; -> return !Cond;
         */
        auto *If = cast<IfStmt>(*First);
        if (!If->hasInitStorage() && !If->hasVarStorage() &&
            !If->isConsteval()) {
          ExprAndBool ThenReturnBool =
              checkSingleStatement(If->getThen(), parseReturnLiteralBool);
          if (ThenReturnBool &&
              ThenReturnBool.Bool != TrailingReturnBool.Bool) {
            if ((Check->ChainedConditionalReturn || !PrevIf) &&
                If->getElse() == nullptr) {
              Check->replaceCompoundReturnWithCondition(
                  Context, cast<ReturnStmt>(*Second), TrailingReturnBool.Bool,
                  If, ThenReturnBool.Item);
            }
          }
        }
      } else if (isa<LabelStmt, CaseStmt, DefaultStmt>(*First)) {
        /*
         * (case X|label_X|default): if (Cond) return BoolLiteral;
         *                           return !BoolLiteral
         */
        Stmt *SubStmt =
            isa<LabelStmt>(*First)  ? cast<LabelStmt>(*First)->getSubStmt()
            : isa<CaseStmt>(*First) ? cast<CaseStmt>(*First)->getSubStmt()
                                    : cast<DefaultStmt>(*First)->getSubStmt();
        auto *SubIf = dyn_cast<IfStmt>(SubStmt);
        if (SubIf && !SubIf->getElse() && !SubIf->hasInitStorage() &&
            !SubIf->hasVarStorage() && !SubIf->isConsteval()) {
          ExprAndBool ThenReturnBool =
              checkSingleStatement(SubIf->getThen(), parseReturnLiteralBool);
          if (ThenReturnBool &&
              ThenReturnBool.Bool != TrailingReturnBool.Bool) {
            Check->replaceCompoundReturnWithCondition(
                Context, cast<ReturnStmt>(*Second), TrailingReturnBool.Bool,
                SubIf, ThenReturnBool.Item);
          }
        }
      }
    }
    return true;
  }

  static bool isUnaryLNot(const Expr *E) {
    return isa<UnaryOperator>(E) &&
           cast<UnaryOperator>(E)->getOpcode() == UO_LNot;
  }

  template <typename Functor>
  static bool checkEitherSide(const BinaryOperator *BO, Functor Func) {
    return Func(BO->getLHS()) || Func(BO->getRHS());
  }

  static bool nestedDemorgan(const Expr *E, unsigned NestingLevel) {
    const auto *BO = dyn_cast<BinaryOperator>(E->IgnoreUnlessSpelledInSource());
    if (!BO)
      return false;
    if (!BO->getType()->isBooleanType())
      return false;
    switch (BO->getOpcode()) {
    case BO_LT:
    case BO_GT:
    case BO_LE:
    case BO_GE:
    case BO_EQ:
    case BO_NE:
      return true;
    case BO_LAnd:
    case BO_LOr:
      if (checkEitherSide(BO, isUnaryLNot))
        return true;
      if (NestingLevel) {
        if (checkEitherSide(BO, [NestingLevel](const Expr *E) {
              return nestedDemorgan(E, NestingLevel - 1);
            }))
          return true;
      }
      return false;
    default:
      return false;
    }
  }

  bool TraverseUnaryOperator(UnaryOperator *Op) {
    if (!Check->SimplifyDeMorgan || Op->getOpcode() != UO_LNot)
      return Base::TraverseUnaryOperator(Op);
    Expr *SubImp = Op->getSubExpr()->IgnoreImplicit();
    auto *Parens = dyn_cast<ParenExpr>(SubImp);
    auto *BinaryOp =
        Parens
            ? dyn_cast<BinaryOperator>(Parens->getSubExpr()->IgnoreImplicit())
            : dyn_cast<BinaryOperator>(SubImp);
    if (!BinaryOp || !BinaryOp->isLogicalOp() ||
        !BinaryOp->getType()->isBooleanType())
      return Base::TraverseUnaryOperator(Op);
    if (Check->SimplifyDeMorganRelaxed ||
        checkEitherSide(BinaryOp, isUnaryLNot) ||
        checkEitherSide(BinaryOp,
                        [](const Expr *E) { return nestedDemorgan(E, 1); })) {
      if (Check->reportDeMorgan(Context, Op, BinaryOp, !IsProcessing, parent(),
                                Parens) &&
          !Check->areDiagsSelfContained()) {
        llvm::SaveAndRestore RAII(IsProcessing, true);
        return Base::TraverseUnaryOperator(Op);
      }
    }
    return Base::TraverseUnaryOperator(Op);
  }

private:
  bool IsProcessing = false;
  SimplifyBooleanExprCheck *Check;
  SmallVector<Stmt *, 32> StmtStack;
  ASTContext &Context;
};

SimplifyBooleanExprCheck::SimplifyBooleanExprCheck(StringRef Name,
                                                   ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      ChainedConditionalReturn(Options.get("ChainedConditionalReturn", false)),
      ChainedConditionalAssignment(
          Options.get("ChainedConditionalAssignment", false)),
      SimplifyDeMorgan(Options.get("SimplifyDeMorgan", true)),
      SimplifyDeMorganRelaxed(Options.get("SimplifyDeMorganRelaxed", false)) {
  if (SimplifyDeMorganRelaxed && !SimplifyDeMorgan)
    configurationDiag("%0: 'SimplifyDeMorganRelaxed' cannot be enabled "
                      "without 'SimplifyDeMorgan' enabled")
        << Name;
}

static bool containsBoolLiteral(const Expr *E) {
  if (!E)
    return false;
  E = E->IgnoreParenImpCasts();
  if (isa<CXXBoolLiteralExpr>(E))
    return true;
  if (const auto *BinOp = dyn_cast<BinaryOperator>(E))
    return containsBoolLiteral(BinOp->getLHS()) ||
           containsBoolLiteral(BinOp->getRHS());
  if (const auto *UnaryOp = dyn_cast<UnaryOperator>(E))
    return containsBoolLiteral(UnaryOp->getSubExpr());
  return false;
}

void SimplifyBooleanExprCheck::reportBinOp(const ASTContext &Context,
                                           const BinaryOperator *Op) {
  const auto *LHS = Op->getLHS()->IgnoreParenImpCasts();
  const auto *RHS = Op->getRHS()->IgnoreParenImpCasts();

  const CXXBoolLiteralExpr *Bool;
  const Expr *Other;
  if ((Bool = dyn_cast<CXXBoolLiteralExpr>(LHS)) != nullptr)
    Other = RHS;
  else if ((Bool = dyn_cast<CXXBoolLiteralExpr>(RHS)) != nullptr)
    Other = LHS;
  else
    return;

  if (Bool->getBeginLoc().isMacroID())
    return;

  // FIXME: why do we need this?
  if (!isa<CXXBoolLiteralExpr>(Other) && containsBoolLiteral(Other))
    return;

  bool BoolValue = Bool->getValue();

  auto ReplaceWithExpression = [this, &Context, LHS, RHS,
                                Bool](const Expr *ReplaceWith, bool Negated) {
    std::string Replacement =
        replacementExpression(Context, Negated, ReplaceWith);
    SourceRange Range(LHS->getBeginLoc(), RHS->getEndLoc());
    issueDiag(Context, Bool->getBeginLoc(), SimplifyOperatorDiagnostic, Range,
              Replacement);
  };

  switch (Op->getOpcode()) {
  case BO_LAnd:
    if (BoolValue)
      // expr && true -> expr
      ReplaceWithExpression(Other, /*Negated=*/false);
    else
      // expr && false -> false
      ReplaceWithExpression(Bool, /*Negated=*/false);
    break;
  case BO_LOr:
    if (BoolValue)
      // expr || true -> true
      ReplaceWithExpression(Bool, /*Negated=*/false);
    else
      // expr || false -> expr
      ReplaceWithExpression(Other, /*Negated=*/false);
    break;
  case BO_EQ:
    // expr == true -> expr, expr == false -> !expr
    ReplaceWithExpression(Other, /*Negated=*/!BoolValue);
    break;
  case BO_NE:
    // expr != true -> !expr, expr != false -> expr
    ReplaceWithExpression(Other, /*Negated=*/BoolValue);
    break;
  default:
    break;
  }
}

void SimplifyBooleanExprCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "ChainedConditionalReturn", ChainedConditionalReturn);
  Options.store(Opts, "ChainedConditionalAssignment",
                ChainedConditionalAssignment);
  Options.store(Opts, "SimplifyDeMorgan", SimplifyDeMorgan);
  Options.store(Opts, "SimplifyDeMorganRelaxed", SimplifyDeMorganRelaxed);
}

void SimplifyBooleanExprCheck::registerMatchers(MatchFinder *Finder) {
  Finder->addMatcher(translationUnitDecl(), this);
}

void SimplifyBooleanExprCheck::check(const MatchFinder::MatchResult &Result) {
  Visitor(this, *Result.Context).traverse();
}

void SimplifyBooleanExprCheck::issueDiag(const ASTContext &Context,
                                         SourceLocation Loc,
                                         StringRef Description,
                                         SourceRange ReplacementRange,
                                         StringRef Replacement) {
  CharSourceRange CharRange =
      Lexer::makeFileCharRange(CharSourceRange::getTokenRange(ReplacementRange),
                               Context.getSourceManager(), getLangOpts());

  DiagnosticBuilder Diag = diag(Loc, Description);
  if (!containsDiscardedTokens(Context, CharRange))
    Diag << FixItHint::CreateReplacement(CharRange, Replacement);
}

void SimplifyBooleanExprCheck::replaceWithThenStatement(
    const ASTContext &Context, const IfStmt *IfStatement,
    const Expr *BoolLiteral) {
  issueDiag(Context, BoolLiteral->getBeginLoc(), SimplifyConditionDiagnostic,
            IfStatement->getSourceRange(),
            getText(Context, *IfStatement->getThen()));
}

void SimplifyBooleanExprCheck::replaceWithElseStatement(
    const ASTContext &Context, const IfStmt *IfStatement,
    const Expr *BoolLiteral) {
  const Stmt *ElseStatement = IfStatement->getElse();
  issueDiag(Context, BoolLiteral->getBeginLoc(), SimplifyConditionDiagnostic,
            IfStatement->getSourceRange(),
            ElseStatement ? getText(Context, *ElseStatement) : "");
}

void SimplifyBooleanExprCheck::replaceWithCondition(
    const ASTContext &Context, const ConditionalOperator *Ternary,
    bool Negated) {
  std::string Replacement =
      replacementExpression(Context, Negated, Ternary->getCond());
  issueDiag(Context, Ternary->getTrueExpr()->getBeginLoc(),
            "redundant boolean literal in ternary expression result",
            Ternary->getSourceRange(), Replacement);
}

void SimplifyBooleanExprCheck::replaceWithReturnCondition(
    const ASTContext &Context, const IfStmt *If, const Expr *BoolLiteral,
    bool Negated) {
  StringRef Terminator = isa<CompoundStmt>(If->getElse()) ? ";" : "";
  std::string Condition =
      replacementExpression(Context, Negated, If->getCond());
  std::string Replacement = ("return " + Condition + Terminator).str();
  SourceLocation Start = BoolLiteral->getBeginLoc();
  issueDiag(Context, Start, SimplifyConditionalReturnDiagnostic,
            If->getSourceRange(), Replacement);
}

void SimplifyBooleanExprCheck::replaceCompoundReturnWithCondition(
    const ASTContext &Context, const ReturnStmt *Ret, bool Negated,
    const IfStmt *If, const Expr *ThenReturn) {
  const std::string Replacement =
      "return " + replacementExpression(Context, Negated, If->getCond());
  issueDiag(Context, ThenReturn->getBeginLoc(),
            SimplifyConditionalReturnDiagnostic,
            SourceRange(If->getBeginLoc(), Ret->getEndLoc()), Replacement);
}

void SimplifyBooleanExprCheck::replaceWithAssignment(const ASTContext &Context,
                                                     const IfStmt *IfAssign,
                                                     const Expr *Var,
                                                     SourceLocation Loc,
                                                     bool Negated) {
  SourceRange Range = IfAssign->getSourceRange();
  StringRef VariableName = getText(Context, *Var);
  StringRef Terminator = isa<CompoundStmt>(IfAssign->getElse()) ? ";" : "";
  std::string Condition =
      replacementExpression(Context, Negated, IfAssign->getCond());
  std::string Replacement =
      (VariableName + " = " + Condition + Terminator).str();
  issueDiag(Context, Loc, "redundant boolean literal in conditional assignment",
            Range, Replacement);
}

/// Swaps a \c BinaryOperator opcode from `&&` to `||` or vice-versa.
static bool flipDemorganOperator(llvm::SmallVectorImpl<FixItHint> &Output,
                                 const BinaryOperator *BO) {
  assert(BO->isLogicalOp());
  if (BO->getOperatorLoc().isMacroID())
    return true;
  Output.push_back(FixItHint::CreateReplacement(
      BO->getOperatorLoc(), BO->getOpcode() == BO_LAnd ? "||" : "&&"));
  return false;
}

static BinaryOperatorKind getDemorganFlippedOperator(BinaryOperatorKind BO) {
  assert(BinaryOperator::isLogicalOp(BO));
  return BO == BO_LAnd ? BO_LOr : BO_LAnd;
}

static bool flipDemorganSide(SmallVectorImpl<FixItHint> &Fixes,
                             const ASTContext &Ctx, const Expr *E,
                             std::optional<BinaryOperatorKind> OuterBO);

/// Inverts \p BinOp, Removing \p Parens if they exist and are safe to remove.
/// returns \c true if there is any issue building the Fixes, \c false
/// otherwise.
static bool
flipDemorganBinaryOperator(SmallVectorImpl<FixItHint> &Fixes,
                           const ASTContext &Ctx, const BinaryOperator *BinOp,
                           std::optional<BinaryOperatorKind> OuterBO,
                           const ParenExpr *Parens = nullptr) {
  switch (BinOp->getOpcode()) {
  case BO_LAnd:
  case BO_LOr: {
    // if we have 'a && b' or 'a || b', use demorgan to flip it to '!a || !b'
    // or '!a && !b'.
    if (flipDemorganOperator(Fixes, BinOp))
      return true;
    auto NewOp = getDemorganFlippedOperator(BinOp->getOpcode());
    if (OuterBO) {
      // The inner parens are technically needed in a fix for
      // `!(!A1 && !(A2 || A3)) -> (A1 || (A2 && A3))`,
      // however this would trip the LogicalOpParentheses warning.
      // FIXME: Make this user configurable or detect if that warning is
      // enabled.
      constexpr bool LogicalOpParentheses = true;
      if (((*OuterBO == NewOp) || (!LogicalOpParentheses &&
                                   (*OuterBO == BO_LOr && NewOp == BO_LAnd))) &&
          Parens) {
        if (!Parens->getLParen().isMacroID() &&
            !Parens->getRParen().isMacroID()) {
          Fixes.push_back(FixItHint::CreateRemoval(Parens->getLParen()));
          Fixes.push_back(FixItHint::CreateRemoval(Parens->getRParen()));
        }
      }
      if (*OuterBO == BO_LAnd && NewOp == BO_LOr && !Parens) {
        Fixes.push_back(FixItHint::CreateInsertion(BinOp->getBeginLoc(), "("));
        Fixes.push_back(FixItHint::CreateInsertion(
            Lexer::getLocForEndOfToken(BinOp->getEndLoc(), 0,
                                       Ctx.getSourceManager(),
                                       Ctx.getLangOpts()),
            ")"));
      }
    }
    if (flipDemorganSide(Fixes, Ctx, BinOp->getLHS(), NewOp) ||
        flipDemorganSide(Fixes, Ctx, BinOp->getRHS(), NewOp))
      return true;
    return false;
  };
  case BO_LT:
  case BO_GT:
  case BO_LE:
  case BO_GE:
  case BO_EQ:
  case BO_NE:
    // For comparison operators, just negate the comparison.
    if (BinOp->getOperatorLoc().isMacroID())
      return true;
    Fixes.push_back(FixItHint::CreateReplacement(
        BinOp->getOperatorLoc(),
        BinaryOperator::getOpcodeStr(
            BinaryOperator::negateComparisonOp(BinOp->getOpcode()))));
    return false;
  default:
    // for any other binary operator, just use logical not and wrap in
    // parens.
    if (Parens) {
      if (Parens->getBeginLoc().isMacroID())
        return true;
      Fixes.push_back(FixItHint::CreateInsertion(Parens->getBeginLoc(), "!"));
    } else {
      if (BinOp->getBeginLoc().isMacroID() || BinOp->getEndLoc().isMacroID())
        return true;
      Fixes.append({FixItHint::CreateInsertion(BinOp->getBeginLoc(), "!("),
                    FixItHint::CreateInsertion(
                        Lexer::getLocForEndOfToken(BinOp->getEndLoc(), 0,
                                                   Ctx.getSourceManager(),
                                                   Ctx.getLangOpts()),
                        ")")});
    }
    break;
  }
  return false;
}

static bool flipDemorganSide(SmallVectorImpl<FixItHint> &Fixes,
                             const ASTContext &Ctx, const Expr *E,
                             std::optional<BinaryOperatorKind> OuterBO) {
  if (isa<UnaryOperator>(E) && cast<UnaryOperator>(E)->getOpcode() == UO_LNot) {
    //  if we have a not operator, '!a', just remove the '!'.
    if (cast<UnaryOperator>(E)->getOperatorLoc().isMacroID())
      return true;
    Fixes.push_back(
        FixItHint::CreateRemoval(cast<UnaryOperator>(E)->getOperatorLoc()));
    return false;
  }
  if (const auto *BinOp = dyn_cast<BinaryOperator>(E)) {
    return flipDemorganBinaryOperator(Fixes, Ctx, BinOp, OuterBO);
  }
  if (const auto *Paren = dyn_cast<ParenExpr>(E)) {
    if (const auto *BinOp = dyn_cast<BinaryOperator>(Paren->getSubExpr())) {
      return flipDemorganBinaryOperator(Fixes, Ctx, BinOp, OuterBO, Paren);
    }
  }
  // Fallback case just insert a logical not operator.
  if (E->getBeginLoc().isMacroID())
    return true;
  Fixes.push_back(FixItHint::CreateInsertion(E->getBeginLoc(), "!"));
  return false;
}

static bool shouldRemoveParens(const Stmt *Parent,
                               BinaryOperatorKind NewOuterBinary,
                               const ParenExpr *Parens) {
  if (!Parens)
    return false;
  if (!Parent)
    return true;
  switch (Parent->getStmtClass()) {
  case Stmt::BinaryOperatorClass: {
    const auto *BO = cast<BinaryOperator>(Parent);
    if (BO->isAssignmentOp())
      return true;
    if (BO->isCommaOp())
      return true;
    if (BO->getOpcode() == NewOuterBinary)
      return true;
    return false;
  }
  case Stmt::UnaryOperatorClass:
  case Stmt::CXXRewrittenBinaryOperatorClass:
    return false;
  default:
    return true;
  }
}

bool SimplifyBooleanExprCheck::reportDeMorgan(const ASTContext &Context,
                                              const UnaryOperator *Outer,
                                              const BinaryOperator *Inner,
                                              bool TryOfferFix,
                                              const Stmt *Parent,
                                              const ParenExpr *Parens) {
  assert(Outer);
  assert(Inner);
  assert(Inner->isLogicalOp());

  auto Diag =
      diag(Outer->getBeginLoc(),
           "boolean expression can be simplified by DeMorgan's theorem");
  Diag << Outer->getSourceRange();
  // If we have already fixed this with a previous fix, don't attempt any fixes
  if (!TryOfferFix)
    return false;
  if (Outer->getOperatorLoc().isMacroID())
    return false;
  SmallVector<FixItHint> Fixes;
  auto NewOpcode = getDemorganFlippedOperator(Inner->getOpcode());
  if (shouldRemoveParens(Parent, NewOpcode, Parens)) {
    Fixes.push_back(FixItHint::CreateRemoval(
        SourceRange(Outer->getOperatorLoc(), Parens->getLParen())));
    Fixes.push_back(FixItHint::CreateRemoval(Parens->getRParen()));
  } else {
    Fixes.push_back(FixItHint::CreateRemoval(Outer->getOperatorLoc()));
  }
  if (flipDemorganOperator(Fixes, Inner))
    return false;
  if (flipDemorganSide(Fixes, Context, Inner->getLHS(), NewOpcode) ||
      flipDemorganSide(Fixes, Context, Inner->getRHS(), NewOpcode))
    return false;
  Diag << Fixes;
  return true;
}
} // namespace clang::tidy::readability
