//===--- OperatorsRepresentationCheck.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 "OperatorsRepresentationCheck.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/STLExtras.h"
#include <array>
#include <utility>

using namespace clang::ast_matchers;

namespace clang::tidy::readability {

static StringRef getOperatorSpelling(SourceLocation Loc, ASTContext &Context) {
  if (Loc.isInvalid())
    return {};

  SourceManager &SM = Context.getSourceManager();

  Loc = SM.getSpellingLoc(Loc);
  if (Loc.isInvalid())
    return {};

  const CharSourceRange TokenRange = CharSourceRange::getTokenRange(Loc);
  return Lexer::getSourceText(TokenRange, SM, Context.getLangOpts());
}

namespace {

AST_MATCHER_P2(BinaryOperator, hasInvalidBinaryOperatorRepresentation,
               BinaryOperatorKind, Kind, llvm::StringRef,
               ExpectedRepresentation) {
  if (Node.getOpcode() != Kind || ExpectedRepresentation.empty())
    return false;

  StringRef Spelling =
      getOperatorSpelling(Node.getOperatorLoc(), Finder->getASTContext());
  return !Spelling.empty() && Spelling != ExpectedRepresentation;
}

AST_MATCHER_P2(UnaryOperator, hasInvalidUnaryOperatorRepresentation,
               UnaryOperatorKind, Kind, llvm::StringRef,
               ExpectedRepresentation) {
  if (Node.getOpcode() != Kind || ExpectedRepresentation.empty())
    return false;

  StringRef Spelling =
      getOperatorSpelling(Node.getOperatorLoc(), Finder->getASTContext());
  return !Spelling.empty() && Spelling != ExpectedRepresentation;
}

AST_MATCHER_P2(CXXOperatorCallExpr, hasInvalidOverloadedOperatorRepresentation,
               OverloadedOperatorKind, Kind, llvm::StringRef,
               ExpectedRepresentation) {
  if (Node.getOperator() != Kind || ExpectedRepresentation.empty())
    return false;

  StringRef Spelling =
      getOperatorSpelling(Node.getOperatorLoc(), Finder->getASTContext());
  return !Spelling.empty() && Spelling != ExpectedRepresentation;
}

} // namespace

constexpr std::array<std::pair<llvm::StringRef, llvm::StringRef>, 2U>
    UnaryRepresentation{{{"!", "not"}, {"~", "compl"}}};

constexpr std::array<std::pair<llvm::StringRef, llvm::StringRef>, 9U>
    OperatorsRepresentation{{{"&&", "and"},
                             {"||", "or"},
                             {"^", "xor"},
                             {"&", "bitand"},
                             {"|", "bitor"},
                             {"&=", "and_eq"},
                             {"|=", "or_eq"},
                             {"!=", "not_eq"},
                             {"^=", "xor_eq"}}};

static llvm::StringRef translate(llvm::StringRef Value) {
  for (const auto &[Traditional, Alternative] : UnaryRepresentation) {
    if (Value == Traditional)
      return Alternative;
    if (Value == Alternative)
      return Traditional;
  }

  for (const auto &[Traditional, Alternative] : OperatorsRepresentation) {
    if (Value == Traditional)
      return Alternative;
    if (Value == Alternative)
      return Traditional;
  }
  return {};
}

static bool isNotOperatorStr(llvm::StringRef Value) {
  return translate(Value).empty();
}

static bool isSeparator(char C) noexcept {
  constexpr llvm::StringRef Separators(" \t\r\n\0()<>{};,");
  return Separators.contains(C);
}

static bool needEscaping(llvm::StringRef Operator) {
  switch (Operator[0]) {
  case '&':
  case '|':
  case '!':
  case '^':
  case '~':
    return false;
  default:
    return true;
  }
}

static llvm::StringRef
getRepresentation(const std::vector<llvm::StringRef> &Config,
                  llvm::StringRef Traditional, llvm::StringRef Alternative) {
  if (llvm::is_contained(Config, Traditional))
    return Traditional;
  if (llvm::is_contained(Config, Alternative))
    return Alternative;
  return {};
}

template <typename T>
static bool isAnyOperatorEnabled(const std::vector<llvm::StringRef> &Config,
                                 const T &Operators) {
  for (const auto &[traditional, alternative] : Operators) {
    if (!getRepresentation(Config, traditional, alternative).empty())
      return true;
  }
  return false;
}

OperatorsRepresentationCheck::OperatorsRepresentationCheck(
    StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      BinaryOperators(
          utils::options::parseStringList(Options.get("BinaryOperators", ""))),
      OverloadedOperators(utils::options::parseStringList(
          Options.get("OverloadedOperators", ""))) {
  llvm::erase_if(BinaryOperators, isNotOperatorStr);
  llvm::erase_if(OverloadedOperators, isNotOperatorStr);
}

void OperatorsRepresentationCheck::storeOptions(
    ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "BinaryOperators",
                utils::options::serializeStringList(BinaryOperators));
  Options.store(Opts, "OverloadedOperators",
                utils::options::serializeStringList(OverloadedOperators));
}

std::optional<TraversalKind>
OperatorsRepresentationCheck::getCheckTraversalKind() const {
  return TK_IgnoreUnlessSpelledInSource;
}

bool OperatorsRepresentationCheck::isLanguageVersionSupported(
    const LangOptions &LangOpts) const {
  return LangOpts.CPlusPlus;
}

void OperatorsRepresentationCheck::registerBinaryOperatorMatcher(
    MatchFinder *Finder) {
  if (!isAnyOperatorEnabled(BinaryOperators, OperatorsRepresentation))
    return;

  Finder->addMatcher(
      binaryOperator(
          unless(isExpansionInSystemHeader()),
          anyOf(hasInvalidBinaryOperatorRepresentation(
                    BO_LAnd, getRepresentation(BinaryOperators, "&&", "and")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_LOr, getRepresentation(BinaryOperators, "||", "or")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_NE, getRepresentation(BinaryOperators, "!=", "not_eq")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_Xor, getRepresentation(BinaryOperators, "^", "xor")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_And, getRepresentation(BinaryOperators, "&", "bitand")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_Or, getRepresentation(BinaryOperators, "|", "bitor")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_AndAssign,
                    getRepresentation(BinaryOperators, "&=", "and_eq")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_OrAssign,
                    getRepresentation(BinaryOperators, "|=", "or_eq")),
                hasInvalidBinaryOperatorRepresentation(
                    BO_XorAssign,
                    getRepresentation(BinaryOperators, "^=", "xor_eq"))))
          .bind("binary_op"),
      this);
}

void OperatorsRepresentationCheck::registerUnaryOperatorMatcher(
    MatchFinder *Finder) {
  if (!isAnyOperatorEnabled(BinaryOperators, UnaryRepresentation))
    return;

  Finder->addMatcher(
      unaryOperator(
          unless(isExpansionInSystemHeader()),
          anyOf(hasInvalidUnaryOperatorRepresentation(
                    UO_LNot, getRepresentation(BinaryOperators, "!", "not")),
                hasInvalidUnaryOperatorRepresentation(
                    UO_Not, getRepresentation(BinaryOperators, "~", "compl"))))
          .bind("unary_op"),
      this);
}

void OperatorsRepresentationCheck::registerOverloadedOperatorMatcher(
    MatchFinder *Finder) {
  if (!isAnyOperatorEnabled(OverloadedOperators, OperatorsRepresentation) &&
      !isAnyOperatorEnabled(OverloadedOperators, UnaryRepresentation))
    return;

  Finder->addMatcher(
      cxxOperatorCallExpr(
          unless(isExpansionInSystemHeader()),
          anyOf(
              hasInvalidOverloadedOperatorRepresentation(
                  OO_AmpAmp,
                  getRepresentation(OverloadedOperators, "&&", "and")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_PipePipe,
                  getRepresentation(OverloadedOperators, "||", "or")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_Exclaim,
                  getRepresentation(OverloadedOperators, "!", "not")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_ExclaimEqual,
                  getRepresentation(OverloadedOperators, "!=", "not_eq")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_Caret, getRepresentation(OverloadedOperators, "^", "xor")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_Amp,
                  getRepresentation(OverloadedOperators, "&", "bitand")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_Pipe,
                  getRepresentation(OverloadedOperators, "|", "bitor")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_AmpEqual,
                  getRepresentation(OverloadedOperators, "&=", "and_eq")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_PipeEqual,
                  getRepresentation(OverloadedOperators, "|=", "or_eq")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_CaretEqual,
                  getRepresentation(OverloadedOperators, "^=", "xor_eq")),
              hasInvalidOverloadedOperatorRepresentation(
                  OO_Tilde,
                  getRepresentation(OverloadedOperators, "~", "compl"))))
          .bind("overloaded_op"),
      this);
}

void OperatorsRepresentationCheck::registerMatchers(MatchFinder *Finder) {
  registerBinaryOperatorMatcher(Finder);
  registerUnaryOperatorMatcher(Finder);
  registerOverloadedOperatorMatcher(Finder);
}

void OperatorsRepresentationCheck::check(
    const MatchFinder::MatchResult &Result) {

  SourceLocation Loc;

  if (const auto *Op = Result.Nodes.getNodeAs<BinaryOperator>("binary_op"))
    Loc = Op->getOperatorLoc();
  else if (const auto *Op = Result.Nodes.getNodeAs<UnaryOperator>("unary_op"))
    Loc = Op->getOperatorLoc();
  else if (const auto *Op =
               Result.Nodes.getNodeAs<CXXOperatorCallExpr>("overloaded_op"))
    Loc = Op->getOperatorLoc();

  if (Loc.isInvalid())
    return;

  Loc = Result.SourceManager->getSpellingLoc(Loc);
  if (Loc.isInvalid() || Loc.isMacroID())
    return;

  const CharSourceRange TokenRange = CharSourceRange::getTokenRange(Loc);
  if (TokenRange.isInvalid())
    return;

  StringRef Spelling = Lexer::getSourceText(TokenRange, *Result.SourceManager,
                                            Result.Context->getLangOpts());
  StringRef TranslatedSpelling = translate(Spelling);

  if (TranslatedSpelling.empty())
    return;

  std::string FixSpelling = TranslatedSpelling.str();

  StringRef SourceRepresentation = "an alternative";
  StringRef TargetRepresentation = "a traditional";
  if (needEscaping(TranslatedSpelling)) {
    SourceRepresentation = "a traditional";
    TargetRepresentation = "an alternative";

    StringRef SpellingEx = Lexer::getSourceText(
        CharSourceRange::getCharRange(
            TokenRange.getBegin().getLocWithOffset(-1),
            TokenRange.getBegin().getLocWithOffset(Spelling.size() + 1U)),
        *Result.SourceManager, Result.Context->getLangOpts());
    if (SpellingEx.empty() || !isSeparator(SpellingEx.front()))
      FixSpelling.insert(FixSpelling.begin(), ' ');
    if (SpellingEx.empty() || !isSeparator(SpellingEx.back()))
      FixSpelling.push_back(' ');
  }

  diag(
      Loc,
      "'%0' is %1 token spelling, consider using %2 token '%3' for consistency")
      << Spelling << SourceRepresentation << TargetRepresentation
      << TranslatedSpelling
      << FixItHint::CreateReplacement(TokenRange, FixSpelling);
}

} // namespace clang::tidy::readability
