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

using namespace clang::ast_matchers;

namespace clang::tidy::misc {

UniqueptrResetReleaseCheck::UniqueptrResetReleaseCheck(
    StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      Inserter(Options.getLocalOrGlobal("IncludeStyle",
                                        utils::IncludeSorter::IS_LLVM),
               areDiagsSelfContained()) {}

void UniqueptrResetReleaseCheck::storeOptions(
    ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IncludeStyle", Inserter.getStyle());
}

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

void UniqueptrResetReleaseCheck::registerMatchers(MatchFinder *Finder) {
  Finder->addMatcher(
      cxxMemberCallExpr(
          callee(memberExpr(
                     member(cxxMethodDecl(
                         hasName("reset"),
                         ofClass(cxxRecordDecl(hasName("::std::unique_ptr"),
                                               decl().bind("left_class"))))))
                     .bind("reset_member")),
          hasArgument(
              0, ignoringParenImpCasts(cxxMemberCallExpr(
                     on(expr().bind("right")),
                     callee(memberExpr(member(cxxMethodDecl(
                                           hasName("release"),
                                           ofClass(cxxRecordDecl(
                                               hasName("::std::unique_ptr"),
                                               decl().bind("right_class"))))))
                                .bind("release_member"))))))
          .bind("reset_call"),
      this);
}

namespace {
const Type *getDeleterForUniquePtr(const MatchFinder::MatchResult &Result,
                                   StringRef ID) {
  const auto *Class =
      Result.Nodes.getNodeAs<ClassTemplateSpecializationDecl>(ID);
  if (!Class)
    return nullptr;
  auto DeleterArgument = Class->getTemplateArgs()[1];
  if (DeleterArgument.getKind() != TemplateArgument::Type)
    return nullptr;
  return DeleterArgument.getAsType().getTypePtr();
}

bool areDeletersCompatible(const MatchFinder::MatchResult &Result) {
  const Type *LeftDeleterType = getDeleterForUniquePtr(Result, "left_class");
  const Type *RightDeleterType = getDeleterForUniquePtr(Result, "right_class");

  if (LeftDeleterType->getUnqualifiedDesugaredType() ==
      RightDeleterType->getUnqualifiedDesugaredType()) {
    // Same type. We assume they are compatible.
    // This check handles the case where the deleters are function pointers.
    return true;
  }

  const CXXRecordDecl *LeftDeleter = LeftDeleterType->getAsCXXRecordDecl();
  const CXXRecordDecl *RightDeleter = RightDeleterType->getAsCXXRecordDecl();
  if (!LeftDeleter || !RightDeleter)
    return false;

  if (LeftDeleter->getCanonicalDecl() == RightDeleter->getCanonicalDecl()) {
    // Same class. We assume they are compatible.
    return true;
  }

  const auto *LeftAsTemplate =
      dyn_cast<ClassTemplateSpecializationDecl>(LeftDeleter);
  const auto *RightAsTemplate =
      dyn_cast<ClassTemplateSpecializationDecl>(RightDeleter);
  if (LeftAsTemplate && RightAsTemplate &&
      LeftAsTemplate->getSpecializedTemplate() ==
          RightAsTemplate->getSpecializedTemplate()) {
    // They are different instantiations of the same template. We assume they
    // are compatible.
    // This handles things like std::default_delete<Base> vs.
    // std::default_delete<Derived>.
    return true;
  }
  return false;
}

} // namespace

void UniqueptrResetReleaseCheck::check(const MatchFinder::MatchResult &Result) {
  if (!areDeletersCompatible(Result))
    return;

  const auto *ResetMember = Result.Nodes.getNodeAs<MemberExpr>("reset_member");
  const auto *ReleaseMember =
      Result.Nodes.getNodeAs<MemberExpr>("release_member");
  const auto *Right = Result.Nodes.getNodeAs<Expr>("right");
  const auto *ResetCall =
      Result.Nodes.getNodeAs<CXXMemberCallExpr>("reset_call");

  StringRef AssignmentText = " = ";
  StringRef TrailingText = "";
  bool NeedsUtilityInclude = false;
  if (ReleaseMember->isArrow()) {
    AssignmentText = " = std::move(*";
    TrailingText = ")";
    NeedsUtilityInclude = true;
  } else if (!Right->isPRValue()) {
    AssignmentText = " = std::move(";
    TrailingText = ")";
    NeedsUtilityInclude = true;
  }

  auto D = diag(ResetMember->getExprLoc(),
                "prefer 'unique_ptr<>' assignment over 'release' and 'reset'");
  if (ResetMember->isArrow())
    D << FixItHint::CreateInsertion(ResetMember->getBeginLoc(), "*");
  D << FixItHint::CreateReplacement(
           CharSourceRange::getCharRange(ResetMember->getOperatorLoc(),
                                         Right->getBeginLoc()),
           AssignmentText)
    << FixItHint::CreateReplacement(
           CharSourceRange::getTokenRange(ReleaseMember->getOperatorLoc(),
                                          ResetCall->getEndLoc()),
           TrailingText);
  if (NeedsUtilityInclude)
    D << Inserter.createIncludeInsertion(
        Result.SourceManager->getFileID(ResetMember->getBeginLoc()),
        "<utility>");
}
} // namespace clang::tidy::misc
