//===--- UseIntegerSignComparisonCheck.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 "UseIntegerSignComparisonCheck.h"
#include "clang/AST/Expr.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"

using namespace clang::ast_matchers;
using namespace clang::ast_matchers::internal;

namespace clang::tidy::modernize {

/// Find if the passed type is the actual "char" type,
/// not applicable to explicit "signed char" or "unsigned char" types.
static bool isActualCharType(const clang::QualType &Ty) {
  using namespace clang;
  const Type *DesugaredType = Ty->getUnqualifiedDesugaredType();
  if (const auto *BT = llvm::dyn_cast<BuiltinType>(DesugaredType))
    return (BT->getKind() == BuiltinType::Char_U ||
            BT->getKind() == BuiltinType::Char_S);
  return false;
}

namespace {
AST_MATCHER(clang::QualType, isActualChar) {
  return clang::tidy::modernize::isActualCharType(Node);
}
} // namespace

static BindableMatcher<clang::Stmt>
intCastExpression(bool IsSigned,
                  const std::string &CastBindName = std::string()) {
  // std::cmp_{} functions trigger a compile-time error if either LHS or RHS
  // is a non-integer type, char, enum or bool
  // (unsigned char/ signed char are Ok and can be used).
  auto IntTypeExpr = expr(hasType(hasCanonicalType(qualType(
      isInteger(), IsSigned ? isSignedInteger() : isUnsignedInteger(),
      unless(isActualChar()), unless(booleanType()), unless(enumType())))));

  const auto ImplicitCastExpr =
      CastBindName.empty() ? implicitCastExpr(hasSourceExpression(IntTypeExpr))
                           : implicitCastExpr(hasSourceExpression(IntTypeExpr))
                                 .bind(CastBindName);

  const auto CStyleCastExpr = cStyleCastExpr(has(ImplicitCastExpr));
  const auto StaticCastExpr = cxxStaticCastExpr(has(ImplicitCastExpr));
  const auto FunctionalCastExpr = cxxFunctionalCastExpr(has(ImplicitCastExpr));

  return expr(anyOf(ImplicitCastExpr, CStyleCastExpr, StaticCastExpr,
                    FunctionalCastExpr));
}

static StringRef parseOpCode(BinaryOperator::Opcode Code) {
  switch (Code) {
  case BO_LT:
    return "cmp_less";
  case BO_GT:
    return "cmp_greater";
  case BO_LE:
    return "cmp_less_equal";
  case BO_GE:
    return "cmp_greater_equal";
  case BO_EQ:
    return "cmp_equal";
  case BO_NE:
    return "cmp_not_equal";
  default:
    return "";
  }
}

UseIntegerSignComparisonCheck::UseIntegerSignComparisonCheck(
    StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
                                               utils::IncludeSorter::IS_LLVM),
                      areDiagsSelfContained()),
      EnableQtSupport(Options.get("EnableQtSupport", false)) {}

void UseIntegerSignComparisonCheck::storeOptions(
    ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
  Options.store(Opts, "EnableQtSupport", EnableQtSupport);
}

void UseIntegerSignComparisonCheck::registerMatchers(MatchFinder *Finder) {
  const auto SignedIntCastExpr = intCastExpression(true, "sIntCastExpression");
  const auto UnSignedIntCastExpr = intCastExpression(false);

  // Flag all operators "==", "<=", ">=", "<", ">", "!="
  // that are used between signed/unsigned
  const auto CompareOperator =
      binaryOperator(hasAnyOperatorName("==", "<=", ">=", "<", ">", "!="),
                     hasOperands(SignedIntCastExpr, UnSignedIntCastExpr),
                     unless(isInTemplateInstantiation()))
          .bind("intComparison");

  Finder->addMatcher(CompareOperator, this);
}

void UseIntegerSignComparisonCheck::registerPPCallbacks(
    const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
  IncludeInserter.registerPreprocessor(PP);
}

void UseIntegerSignComparisonCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *SignedCastExpression =
      Result.Nodes.getNodeAs<ImplicitCastExpr>("sIntCastExpression");
  assert(SignedCastExpression);

  // Ignore the match if we know that the signed int value is not negative.
  Expr::EvalResult EVResult;
  if (!SignedCastExpression->isValueDependent() &&
      SignedCastExpression->getSubExpr()->EvaluateAsInt(EVResult,
                                                        *Result.Context)) {
    const llvm::APSInt SValue = EVResult.Val.getInt();
    if (SValue.isNonNegative())
      return;
  }

  const auto *BinaryOp =
      Result.Nodes.getNodeAs<BinaryOperator>("intComparison");
  if (BinaryOp == nullptr)
    return;

  const BinaryOperator::Opcode OpCode = BinaryOp->getOpcode();

  const Expr *LHS = BinaryOp->getLHS()->IgnoreImpCasts();
  const Expr *RHS = BinaryOp->getRHS()->IgnoreImpCasts();
  if (LHS == nullptr || RHS == nullptr)
    return;
  const Expr *SubExprLHS = nullptr;
  const Expr *SubExprRHS = nullptr;
  SourceRange R1 = SourceRange(LHS->getBeginLoc());
  SourceRange R2 = SourceRange(BinaryOp->getOperatorLoc());
  SourceRange R3 = SourceRange(Lexer::getLocForEndOfToken(
      RHS->getEndLoc(), 0, *Result.SourceManager, getLangOpts()));
  if (const auto *LHSCast = llvm::dyn_cast<ExplicitCastExpr>(LHS)) {
    SubExprLHS = LHSCast->getSubExpr();
    R1 = SourceRange(LHS->getBeginLoc(),
                     SubExprLHS->getBeginLoc().getLocWithOffset(-1));
    R2.setBegin(Lexer::getLocForEndOfToken(
        SubExprLHS->getEndLoc(), 0, *Result.SourceManager, getLangOpts()));
  }
  if (const auto *RHSCast = llvm::dyn_cast<ExplicitCastExpr>(RHS)) {
    SubExprRHS = RHSCast->getSubExpr();
    R2.setEnd(SubExprRHS->getBeginLoc().getLocWithOffset(-1));
  }
  DiagnosticBuilder Diag =
      diag(BinaryOp->getBeginLoc(),
           "comparison between 'signed' and 'unsigned' integers");
  std::string CmpNamespace;
  llvm::StringRef CmpHeader;

  if (getLangOpts().CPlusPlus20) {
    CmpHeader = "<utility>";
    CmpNamespace = llvm::Twine("std::" + parseOpCode(OpCode)).str();
  } else if (getLangOpts().CPlusPlus17 && EnableQtSupport) {
    CmpHeader = "<QtCore/q20utility.h>";
    CmpNamespace = llvm::Twine("q20::" + parseOpCode(OpCode)).str();
  }

  // Prefer modernize-use-integer-sign-comparison when C++20 is available!
  Diag << FixItHint::CreateReplacement(
      CharSourceRange(R1, SubExprLHS != nullptr),
      llvm::Twine(CmpNamespace + "(").str());
  Diag << FixItHint::CreateReplacement(R2, ",");
  Diag << FixItHint::CreateReplacement(CharSourceRange::getCharRange(R3), ")");

  // If there is no include for cmp_{*} functions, we'll add it.
  Diag << IncludeInserter.createIncludeInsertion(
      Result.SourceManager->getFileID(BinaryOp->getBeginLoc()), CmpHeader);
}

} // namespace clang::tidy::modernize
