//===--- ReservedIdentifierCheck.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 "ReservedIdentifierCheck.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Token.h"
#include <algorithm>
#include <cctype>
#include <optional>

// FixItHint

using namespace clang::ast_matchers;

namespace clang::tidy::bugprone {

static const char DoubleUnderscoreTag[] = "du";
static const char UnderscoreCapitalTag[] = "uc";
static const char GlobalUnderscoreTag[] = "global-under";
static const char NonReservedTag[] = "non-reserved";

static const char Message[] =
    "declaration uses identifier '%0', which is %select{a reserved "
    "identifier|not a reserved identifier|reserved in the global namespace}1";

static int getMessageSelectIndex(StringRef Tag) {
  if (Tag == NonReservedTag)
    return 1;
  if (Tag == GlobalUnderscoreTag)
    return 2;
  return 0;
}

llvm::SmallVector<llvm::Regex>
ReservedIdentifierCheck::parseAllowedIdentifiers() const {
  llvm::SmallVector<llvm::Regex> AllowedIdentifiers;
  AllowedIdentifiers.reserve(AllowedIdentifiersRaw.size());

  for (const auto &Identifier : AllowedIdentifiersRaw) {
    AllowedIdentifiers.emplace_back(Identifier.str());
    if (!AllowedIdentifiers.back().isValid()) {
      configurationDiag("Invalid allowed identifier regex '%0'") << Identifier;
      AllowedIdentifiers.pop_back();
    }
  }

  return AllowedIdentifiers;
}

ReservedIdentifierCheck::ReservedIdentifierCheck(StringRef Name,
                                                 ClangTidyContext *Context)
    : RenamerClangTidyCheck(Name, Context),
      Invert(Options.get("Invert", false)),
      AllowedIdentifiersRaw(utils::options::parseStringList(
          Options.get("AllowedIdentifiers", ""))),
      AllowedIdentifiers(parseAllowedIdentifiers()) {}

void ReservedIdentifierCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  RenamerClangTidyCheck::storeOptions(Opts);
  Options.store(Opts, "Invert", Invert);
  Options.store(Opts, "AllowedIdentifiers",
                utils::options::serializeStringList(AllowedIdentifiersRaw));
}

static std::string collapseConsecutive(StringRef Str, char C) {
  std::string Result;
  std::unique_copy(Str.begin(), Str.end(), std::back_inserter(Result),
                   [C](char A, char B) { return A == C && B == C; });
  return Result;
}

static bool hasReservedDoubleUnderscore(StringRef Name,
                                        const LangOptions &LangOpts) {
  if (LangOpts.CPlusPlus)
    return Name.contains("__");
  return Name.starts_with("__");
}

static std::optional<std::string>
getDoubleUnderscoreFixup(StringRef Name, const LangOptions &LangOpts) {
  if (hasReservedDoubleUnderscore(Name, LangOpts))
    return collapseConsecutive(Name, '_');
  return std::nullopt;
}

static bool startsWithUnderscoreCapital(StringRef Name) {
  return Name.size() >= 2 && Name[0] == '_' && std::isupper(Name[1]);
}

static std::optional<std::string> getUnderscoreCapitalFixup(StringRef Name) {
  if (startsWithUnderscoreCapital(Name))
    return std::string(Name.drop_front(1));
  return std::nullopt;
}

static bool startsWithUnderscoreInGlobalNamespace(StringRef Name,
                                                  bool IsInGlobalNamespace,
                                                  bool IsMacro) {
  return !IsMacro && IsInGlobalNamespace && Name.starts_with("_");
}

static std::optional<std::string>
getUnderscoreGlobalNamespaceFixup(StringRef Name, bool IsInGlobalNamespace,
                                  bool IsMacro) {
  if (startsWithUnderscoreInGlobalNamespace(Name, IsInGlobalNamespace, IsMacro))
    return std::string(Name.drop_front(1));
  return std::nullopt;
}

static std::string getNonReservedFixup(std::string Name) {
  assert(!Name.empty());
  if (Name[0] == '_' || std::isupper(Name[0]))
    Name.insert(Name.begin(), '_');
  else
    Name.insert(Name.begin(), 2, '_');
  return Name;
}

static std::optional<RenamerClangTidyCheck::FailureInfo>
getFailureInfoImpl(StringRef Name, bool IsInGlobalNamespace, bool IsMacro,
                   const LangOptions &LangOpts, bool Invert,
                   ArrayRef<llvm::Regex> AllowedIdentifiers) {
  assert(!Name.empty());

  if (llvm::any_of(AllowedIdentifiers, [&](const llvm::Regex &Regex) {
        return Regex.match(Name);
      })) {
    return std::nullopt;
  }
  // TODO: Check for names identical to language keywords, and other names
  // specifically reserved by language standards, e.g. C++ 'zombie names' and C
  // future library directions

  using FailureInfo = RenamerClangTidyCheck::FailureInfo;
  if (!Invert) {
    std::optional<FailureInfo> Info;
    auto AppendFailure = [&](StringRef Kind, std::string &&Fixup) {
      if (!Info) {
        Info = FailureInfo{std::string(Kind), std::move(Fixup)};
      } else {
        Info->KindName += Kind;
        Info->Fixup = std::move(Fixup);
      }
    };
    auto InProgressFixup = [&] {
      return llvm::transformOptional(
                 Info,
                 [](const FailureInfo &Info) { return StringRef(Info.Fixup); })
          .value_or(Name);
    };
    if (auto Fixup = getDoubleUnderscoreFixup(InProgressFixup(), LangOpts))
      AppendFailure(DoubleUnderscoreTag, std::move(*Fixup));
    if (auto Fixup = getUnderscoreCapitalFixup(InProgressFixup()))
      AppendFailure(UnderscoreCapitalTag, std::move(*Fixup));
    if (auto Fixup = getUnderscoreGlobalNamespaceFixup(
            InProgressFixup(), IsInGlobalNamespace, IsMacro))
      AppendFailure(GlobalUnderscoreTag, std::move(*Fixup));

    return Info;
  }
  if (!(hasReservedDoubleUnderscore(Name, LangOpts) ||
        startsWithUnderscoreCapital(Name) ||
        startsWithUnderscoreInGlobalNamespace(Name, IsInGlobalNamespace,
                                              IsMacro)))
    return FailureInfo{NonReservedTag, getNonReservedFixup(std::string(Name))};
  return std::nullopt;
}

std::optional<RenamerClangTidyCheck::FailureInfo>
ReservedIdentifierCheck::getDeclFailureInfo(const NamedDecl *Decl,
                                            const SourceManager &) const {
  assert(Decl && Decl->getIdentifier() && !Decl->getName().empty() &&
         "Decl must be an explicit identifier with a name.");
  // Implicit identifiers cannot fail.
  if (Decl->isImplicit())
    return std::nullopt;

  return getFailureInfoImpl(
      Decl->getName(), isa<TranslationUnitDecl>(Decl->getDeclContext()),
      /*IsMacro = */ false, getLangOpts(), Invert, AllowedIdentifiers);
}

std::optional<RenamerClangTidyCheck::FailureInfo>
ReservedIdentifierCheck::getMacroFailureInfo(const Token &MacroNameTok,
                                             const SourceManager &) const {
  return getFailureInfoImpl(MacroNameTok.getIdentifierInfo()->getName(), true,
                            /*IsMacro = */ true, getLangOpts(), Invert,
                            AllowedIdentifiers);
}

RenamerClangTidyCheck::DiagInfo
ReservedIdentifierCheck::getDiagInfo(const NamingCheckId &ID,
                                     const NamingCheckFailure &Failure) const {
  return DiagInfo{Message, [&](DiagnosticBuilder &Diag) {
                    Diag << ID.second
                         << getMessageSelectIndex(Failure.Info.KindName);
                  }};
}

} // namespace clang::tidy::bugprone
