//===--- CrtpConstructorAccessibilityCheck.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 "CrtpConstructorAccessibilityCheck.h"
#include "../utils/LexerUtils.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"

using namespace clang::ast_matchers;

namespace clang::tidy::bugprone {

static bool hasPrivateConstructor(const CXXRecordDecl *RD) {
  return llvm::any_of(RD->ctors(), [](const CXXConstructorDecl *Ctor) {
    return Ctor->getAccess() == AS_private;
  });
}

static bool isDerivedParameterBefriended(const CXXRecordDecl *CRTP,
                                         const NamedDecl *Param) {
  return llvm::any_of(CRTP->friends(), [&](const FriendDecl *Friend) {
    const TypeSourceInfo *const FriendType = Friend->getFriendType();
    if (!FriendType) {
      return false;
    }

    const auto *const TTPT =
        dyn_cast<TemplateTypeParmType>(FriendType->getType());

    return TTPT && TTPT->getDecl() == Param;
  });
}

static bool isDerivedClassBefriended(const CXXRecordDecl *CRTP,
                                     const CXXRecordDecl *Derived) {
  return llvm::any_of(CRTP->friends(), [&](const FriendDecl *Friend) {
    const TypeSourceInfo *const FriendType = Friend->getFriendType();
    if (!FriendType) {
      return false;
    }

    return FriendType->getType()->getAsCXXRecordDecl() == Derived;
  });
}

static const NamedDecl *
getDerivedParameter(const ClassTemplateSpecializationDecl *CRTP,
                    const CXXRecordDecl *Derived) {
  size_t Idx = 0;
  const bool AnyOf = llvm::any_of(
      CRTP->getTemplateArgs().asArray(), [&](const TemplateArgument &Arg) {
        ++Idx;
        return Arg.getKind() == TemplateArgument::Type &&
               Arg.getAsType()->getAsCXXRecordDecl() == Derived;
      });

  return AnyOf ? CRTP->getSpecializedTemplate()
                     ->getTemplateParameters()
                     ->getParam(Idx - 1)
               : nullptr;
}

static std::vector<FixItHint>
hintMakeCtorPrivate(const CXXConstructorDecl *Ctor,
                    const std::string &OriginalAccess) {
  std::vector<FixItHint> Hints;

  Hints.emplace_back(FixItHint::CreateInsertion(
      Ctor->getBeginLoc().getLocWithOffset(-1), "private:\n"));

  const ASTContext &ASTCtx = Ctor->getASTContext();
  const SourceLocation CtorEndLoc =
      Ctor->isExplicitlyDefaulted()
          ? utils::lexer::findNextTerminator(Ctor->getEndLoc(),
                                             ASTCtx.getSourceManager(),
                                             ASTCtx.getLangOpts())
          : Ctor->getEndLoc();
  Hints.emplace_back(FixItHint::CreateInsertion(
      CtorEndLoc.getLocWithOffset(1), '\n' + OriginalAccess + ':' + '\n'));

  return Hints;
}

void CrtpConstructorAccessibilityCheck::registerMatchers(MatchFinder *Finder) {
  Finder->addMatcher(
      classTemplateSpecializationDecl(
          decl().bind("crtp"),
          hasAnyTemplateArgument(refersToType(recordType(hasDeclaration(
              cxxRecordDecl(
                  isDerivedFrom(cxxRecordDecl(equalsBoundNode("crtp"))))
                  .bind("derived")))))),
      this);
}

void CrtpConstructorAccessibilityCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *CRTPInstantiation =
      Result.Nodes.getNodeAs<ClassTemplateSpecializationDecl>("crtp");
  const auto *DerivedRecord = Result.Nodes.getNodeAs<CXXRecordDecl>("derived");
  const CXXRecordDecl *CRTPDeclaration =
      CRTPInstantiation->getSpecializedTemplate()->getTemplatedDecl();

  if (!CRTPDeclaration->hasDefinition()) {
    return;
  }

  const auto *DerivedTemplateParameter =
      getDerivedParameter(CRTPInstantiation, DerivedRecord);

  assert(DerivedTemplateParameter &&
         "No template parameter corresponds to the derived class of the CRTP.");

  bool NeedsFriend = !isDerivedParameterBefriended(CRTPDeclaration,
                                                   DerivedTemplateParameter) &&
                     !isDerivedClassBefriended(CRTPDeclaration, DerivedRecord);

  const FixItHint HintFriend = FixItHint::CreateInsertion(
      CRTPDeclaration->getBraceRange().getEnd(),
      "friend " + DerivedTemplateParameter->getNameAsString() + ';' + '\n');

  if (hasPrivateConstructor(CRTPDeclaration) && NeedsFriend) {
    diag(CRTPDeclaration->getLocation(),
         "the CRTP cannot be constructed from the derived class; consider "
         "declaring the derived class as friend")
        << HintFriend;
  }

  auto WithFriendHintIfNeeded = [&](const DiagnosticBuilder &Diag,
                                    bool NeedsFriend) {
    if (NeedsFriend)
      Diag << HintFriend;
  };

  if (!CRTPDeclaration->hasUserDeclaredConstructor()) {
    const bool IsStruct = CRTPDeclaration->isStruct();

    WithFriendHintIfNeeded(
        diag(CRTPDeclaration->getLocation(),
             "the implicit default constructor of the CRTP is publicly "
             "accessible; consider making it private%select{| and declaring "
             "the derived class as friend}0")
            << NeedsFriend
            << FixItHint::CreateInsertion(
                   CRTPDeclaration->getBraceRange().getBegin().getLocWithOffset(
                       1),
                   (IsStruct ? "\nprivate:\n" : "\n") +
                       CRTPDeclaration->getNameAsString() + "() = default;\n" +
                       (IsStruct ? "public:\n" : "")),
        NeedsFriend);
  }

  for (auto &&Ctor : CRTPDeclaration->ctors()) {
    if (Ctor->getAccess() == AS_private || Ctor->isDeleted())
      continue;

    const bool IsPublic = Ctor->getAccess() == AS_public;
    const std::string Access = IsPublic ? "public" : "protected";

    WithFriendHintIfNeeded(
        diag(Ctor->getLocation(),
             "%0 constructor allows the CRTP to be %select{inherited "
             "from|constructed}1 as a regular template class; consider making "
             "it private%select{| and declaring the derived class as friend}2")
            << Access << IsPublic << NeedsFriend
            << hintMakeCtorPrivate(Ctor, Access),
        NeedsFriend);
  }
}

bool CrtpConstructorAccessibilityCheck::isLanguageVersionSupported(
    const LangOptions &LangOpts) const {
  return LangOpts.CPlusPlus11;
}
} // namespace clang::tidy::bugprone
