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

using namespace clang::ast_matchers;

namespace clang::tidy::modernize {

// FIXME: Add chrono::treat_as_floating_point_v and chrono::is_clock_v.
// This will require restructuring the code to handle type traits not
// defined directly in std.
static const llvm::StringSet<> ValueTraits = {
    "alignment_of",
    "conjunction",
    "disjunction",
    "extent",
    "has_unique_object_representations",
    "has_virtual_destructor",
    "is_abstract",
    "is_aggregate",
    "is_arithmetic",
    "is_array",
    "is_assignable",
    "is_base_of",
    "is_bind_expression",
    "is_bounded_array",
    "is_class",
    "is_compound",
    "is_const",
    "is_constructible",
    "is_convertible",
    "is_copy_assignable",
    "is_copy_constructible",
    "is_default_constructible",
    "is_destructible",
    "is_empty",
    "is_enum",
    "is_error_code_enum",
    "is_error_condition_enum",
    "is_execution_policy",
    "is_final",
    "is_floating_point",
    "is_function",
    "is_fundamental",
    "is_implicit_lifetime",
    "is_integral",
    "is_invocable",
    "is_invocable_r",
    "is_layout_compatible",
    "is_lvalue_reference",
    "is_member_function_pointer",
    "is_member_object_pointer",
    "is_member_pointer",
    "is_move_assignable",
    "is_move_constructible",
    "is_nothrow_assignable",
    "is_nothrow_constructible",
    "is_nothrow_convertible",
    "is_nothrow_copy_assignable",
    "is_nothrow_copy_constructible",
    "is_nothrow_default_constructible",
    "is_nothrow_destructible",
    "is_nothrow_invocable",
    "is_nothrow_invocable_r",
    "is_nothrow_move_assignable",
    "is_nothrow_move_constructible",
    "is_nothrow_relocatable",
    "is_nothrow_swappable",
    "is_nothrow_swappable_with",
    "is_null_pointer",
    "is_object",
    "is_placeholder",
    "is_pointer",
    "is_pointer_interconvertible_base_of",
    "is_polymorphic",
    "is_reference",
    "is_replaceable",
    "is_rvalue_reference",
    "is_same",
    "is_scalar",
    "is_scoped_enum",
    "is_signed",
    "is_standard_layout",
    "is_swappable",
    "is_swappable_with",
    "is_trivial",
    "is_trivially_assignable",
    "is_trivially_constructible",
    "is_trivially_copy_assignable",
    "is_trivially_copy_constructible",
    "is_trivially_copyable",
    "is_trivially_default_constructible",
    "is_trivially_destructible",
    "is_trivially_move_assignable",
    "is_trivially_move_constructible",
    "is_trivially_relocatable",
    "is_unbounded_array",
    "is_union",
    "is_unsigned",
    "is_virtual_base_of",
    "is_void",
    "is_volatile",
    "negation",
    "rank",
    "ratio_equal",
    "ratio_greater_equal",
    "ratio_greater",
    "ratio_less_equal",
    "ratio_less",
    "ratio_not_equal",
    "reference_constructs_from_temporary",
    "reference_converts_from_temporary",
    "tuple_size",
    "uses_allocator",
    "variant_size",
};

static const llvm::StringSet<> TypeTraits = {
    "remove_cv",
    "remove_const",
    "remove_volatile",
    "add_cv",
    "add_const",
    "add_volatile",
    "remove_reference",
    "add_lvalue_reference",
    "add_rvalue_reference",
    "remove_pointer",
    "add_pointer",
    "make_signed",
    "make_unsigned",
    "remove_extent",
    "remove_all_extents",
    "aligned_storage",
    "aligned_union",
    "decay",
    "remove_cvref",
    "enable_if",
    "conditional",
    "common_type",
    "common_reference",
    "underlying_type",
    "result_of",
    "invoke_result",
    "type_identity",
    "compare_three_way_result",
    "common_comparison_category",
    "unwrap_ref_decay",
    "unwrap_reference",
    "tuple_element",
    "variant_alternative",
};

static DeclarationName getName(const DependentScopeDeclRefExpr &D) {
  return D.getDeclName();
}

static DeclarationName getName(const DeclRefExpr &D) {
  return D.getDecl()->getDeclName();
}

namespace {
AST_POLYMORPHIC_MATCHER(isValue, AST_POLYMORPHIC_SUPPORTED_TYPES(
                                     DeclRefExpr, DependentScopeDeclRefExpr)) {
  const IdentifierInfo *Ident = getName(Node).getAsIdentifierInfo();
  return Ident && Ident->isStr("value");
}

AST_MATCHER(TypeLoc, isType) {
  if (auto TL = Node.getAs<TypedefTypeLoc>()) {
    const auto *TD = TL.getDecl();
    return TD->getDeclName().isIdentifier() && TD->getName() == "type";
  }
  if (auto TL = Node.getAs<DependentNameTypeLoc>())
    return TL.getTypePtr()->getIdentifier()->getName() == "type";
  return false;
}
} // namespace

static constexpr char Bind[] = "";

void TypeTraitsCheck::registerMatchers(MatchFinder *Finder) {
  // Only register matchers for trait<...>::value in c++17 mode.
  if (getLangOpts().CPlusPlus17) {
    Finder->addMatcher(mapAnyOf(declRefExpr, dependentScopeDeclRefExpr)
                           .with(isValue())
                           .bind(Bind),
                       this);
  }
  Finder->addMatcher(typeLoc(isType()).bind(Bind), this);
}

static bool isNamedDeclInStdTraitsSet(const NamedDecl *ND,
                                      const llvm::StringSet<> &Set) {
  return ND->isInStdNamespace() && ND->getDeclName().isIdentifier() &&
         Set.contains(ND->getName());
}

static bool checkTemplatedDecl(NestedNameSpecifier NNS,
                               const llvm::StringSet<> &Set) {
  if (NNS.getKind() != NestedNameSpecifier::Kind::Type)
    return false;
  const auto *TST = NNS.getAsType()->getAs<TemplateSpecializationType>();
  if (!TST)
    return false;
  if (const TemplateDecl *TD = TST->getTemplateName().getAsTemplateDecl()) {
    return isNamedDeclInStdTraitsSet(TD, Set);
  }
  return false;
}

TypeTraitsCheck::TypeTraitsCheck(StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IgnoreMacros(Options.get("IgnoreMacros", false)) {}

void TypeTraitsCheck::check(const MatchFinder::MatchResult &Result) {
  auto EmitValueWarning = [this, &Result](const NestedNameSpecifierLoc &QualLoc,
                                          SourceLocation EndLoc) {
    SourceLocation TemplateNameEndLoc;
    if (auto TSTL =
            QualLoc.getAsTypeLoc().getAs<TemplateSpecializationTypeLoc>())
      TemplateNameEndLoc = Lexer::getLocForEndOfToken(
          TSTL.getTemplateNameLoc(), 0, *Result.SourceManager,
          Result.Context->getLangOpts());
    else
      return;

    if (EndLoc.isMacroID() || QualLoc.getEndLoc().isMacroID() ||
        TemplateNameEndLoc.isMacroID()) {
      if (IgnoreMacros)
        return;
      diag(QualLoc.getBeginLoc(), "use c++17 style variable templates");
      return;
    }
    diag(QualLoc.getBeginLoc(), "use c++17 style variable templates")
        << FixItHint::CreateInsertion(TemplateNameEndLoc, "_v")
        << FixItHint::CreateRemoval({QualLoc.getEndLoc(), EndLoc});
  };

  auto EmitTypeWarning = [this, &Result](const NestedNameSpecifierLoc &QualLoc,
                                         SourceLocation EndLoc,
                                         SourceLocation TypenameLoc) {
    SourceLocation TemplateNameEndLoc;
    if (auto TSTL =
            QualLoc.getAsTypeLoc().getAs<TemplateSpecializationTypeLoc>())
      TemplateNameEndLoc = Lexer::getLocForEndOfToken(
          TSTL.getTemplateNameLoc(), 0, *Result.SourceManager,
          Result.Context->getLangOpts());
    else
      return;

    if (EndLoc.isMacroID() || QualLoc.getEndLoc().isMacroID() ||
        TemplateNameEndLoc.isMacroID() || TypenameLoc.isMacroID()) {
      if (IgnoreMacros)
        return;
      diag(QualLoc.getBeginLoc(), "use c++14 style type templates");
      return;
    }
    auto Diag = diag(QualLoc.getBeginLoc(), "use c++14 style type templates");

    if (TypenameLoc.isValid())
      Diag << FixItHint::CreateRemoval(TypenameLoc);
    Diag << FixItHint::CreateInsertion(TemplateNameEndLoc, "_t")
         << FixItHint::CreateRemoval({QualLoc.getEndLoc(), EndLoc});
  };

  if (const auto *DRE = Result.Nodes.getNodeAs<DeclRefExpr>(Bind)) {
    if (!DRE->hasQualifier())
      return;
    if (const auto *CTSD = dyn_cast_if_present<ClassTemplateSpecializationDecl>(
            DRE->getQualifier().getAsRecordDecl())) {
      if (isNamedDeclInStdTraitsSet(CTSD, ValueTraits))
        EmitValueWarning(DRE->getQualifierLoc(), DRE->getEndLoc());
    }
    return;
  }

  if (const auto *TL = Result.Nodes.getNodeAs<TypedefTypeLoc>(Bind)) {
    const NestedNameSpecifierLoc QualLoc = TL->getQualifierLoc();
    NestedNameSpecifier NNS = QualLoc.getNestedNameSpecifier();
    if (const auto *CTSD = dyn_cast_if_present<ClassTemplateSpecializationDecl>(
            NNS.getAsRecordDecl())) {
      if (isNamedDeclInStdTraitsSet(CTSD, TypeTraits))
        EmitTypeWarning(TL->getQualifierLoc(), TL->getEndLoc(),
                        TL->getElaboratedKeywordLoc());
    }
    return;
  }

  if (const auto *DSDRE =
          Result.Nodes.getNodeAs<DependentScopeDeclRefExpr>(Bind)) {
    if (checkTemplatedDecl(DSDRE->getQualifier(), ValueTraits))
      EmitValueWarning(DSDRE->getQualifierLoc(), DSDRE->getEndLoc());
    return;
  }

  if (const auto *DNTL = Result.Nodes.getNodeAs<DependentNameTypeLoc>(Bind)) {
    NestedNameSpecifierLoc QualLoc = DNTL->getQualifierLoc();
    if (checkTemplatedDecl(QualLoc.getNestedNameSpecifier(), TypeTraits))
      EmitTypeWarning(QualLoc, DNTL->getEndLoc(),
                      DNTL->getElaboratedKeywordLoc());
    return;
  }
}

void TypeTraitsCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IgnoreMacros", IgnoreMacros);
}
} // namespace clang::tidy::modernize
