//===----------------------------------------------------------------------===//
//
// 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 "MoveConstArgCheck.h"

#include "clang/Lex/Lexer.h"

using namespace clang::ast_matchers;

namespace clang::tidy::performance {

static void replaceCallWithArg(const CallExpr *Call, DiagnosticBuilder &Diag,
                               const SourceManager &SM,
                               const LangOptions &LangOpts) {
  const Expr *Arg = Call->getArg(0);

  const CharSourceRange BeforeArgumentsRange = Lexer::makeFileCharRange(
      CharSourceRange::getCharRange(Call->getBeginLoc(), Arg->getBeginLoc()),
      SM, LangOpts);
  const CharSourceRange AfterArgumentsRange = Lexer::makeFileCharRange(
      CharSourceRange::getCharRange(Call->getEndLoc(),
                                    Call->getEndLoc().getLocWithOffset(1)),
      SM, LangOpts);

  if (BeforeArgumentsRange.isValid() && AfterArgumentsRange.isValid()) {
    Diag << FixItHint::CreateRemoval(BeforeArgumentsRange)
         << FixItHint::CreateRemoval(AfterArgumentsRange);
  }
}

void MoveConstArgCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "CheckTriviallyCopyableMove", CheckTriviallyCopyableMove);
  Options.store(Opts, "CheckMoveToConstRef", CheckMoveToConstRef);
}

void MoveConstArgCheck::registerMatchers(MatchFinder *Finder) {
  auto MoveCallMatcher =
      callExpr(callee(functionDecl(hasName("::std::move"))), argumentCountIs(1),
               unless(isInTemplateInstantiation()))
          .bind("call-move");

  // Match ternary expressions where either branch contains std::move
  auto TernaryWithMoveMatcher =
      conditionalOperator(hasDescendant(MoveCallMatcher));

  Finder->addMatcher(
      expr(anyOf(
          castExpr(hasSourceExpression(MoveCallMatcher)),
          cxxConstructExpr(hasDeclaration(cxxConstructorDecl(anyOf(
                               isCopyConstructor(), isMoveConstructor()))),
                           hasArgument(0, MoveCallMatcher)))),
      this);

  auto ConstTypeParmMatcher =
      qualType(references(isConstQualified())).bind("invocation-parm-type");
  auto RValueTypeParmMatcher =
      qualType(rValueReferenceType()).bind("invocation-parm-type");
  // Matches respective ParmVarDecl for a CallExpr or CXXConstructExpr.
  auto ArgumentWithParamMatcher = forEachArgumentWithParam(
      anyOf(MoveCallMatcher, TernaryWithMoveMatcher),
      parmVarDecl(
          anyOf(hasType(ConstTypeParmMatcher), hasType(RValueTypeParmMatcher)))
          .bind("invocation-parm"));
  // Matches respective types of arguments for a CallExpr or CXXConstructExpr
  // and it works on calls through function pointers as well.
  auto ArgumentWithParamTypeMatcher = forEachArgumentWithParamType(
      anyOf(MoveCallMatcher, TernaryWithMoveMatcher),
      anyOf(ConstTypeParmMatcher, RValueTypeParmMatcher));

  Finder->addMatcher(
      invocation(anyOf(ArgumentWithParamMatcher, ArgumentWithParamTypeMatcher))
          .bind("receiving-expr"),
      this);
}

static bool isRValueReferenceParam(const Expr *Invocation,
                                   const QualType *InvocationParmType,
                                   const Expr *Arg) {
  if (Invocation && (*InvocationParmType)->isRValueReferenceType() &&
      Arg->isLValue()) {
    if (!Invocation->getType()->isRecordType())
      return true;
    if (const auto *ConstructCallExpr =
            dyn_cast<CXXConstructExpr>(Invocation)) {
      if (const auto *ConstructorDecl = ConstructCallExpr->getConstructor()) {
        if (!ConstructorDecl->isCopyOrMoveConstructor() &&
            !ConstructorDecl->isDefaultConstructor())
          return true;
      }
    }
  }
  return false;
}

void MoveConstArgCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *CallMove = Result.Nodes.getNodeAs<CallExpr>("call-move");
  const auto *ReceivingExpr = Result.Nodes.getNodeAs<Expr>("receiving-expr");
  const auto *InvocationParm =
      Result.Nodes.getNodeAs<ParmVarDecl>("invocation-parm");
  const auto *InvocationParmType =
      Result.Nodes.getNodeAs<QualType>("invocation-parm-type");

  // Skipping matchers which have been matched.
  if (!ReceivingExpr && AlreadyCheckedMoves.contains(CallMove))
    return;

  if (ReceivingExpr)
    AlreadyCheckedMoves.insert(CallMove);

  const Expr *Arg = CallMove->getArg(0);
  const QualType ArgType = Arg->getType().getCanonicalType();
  const SourceManager &SM = Result.Context->getSourceManager();

  const CharSourceRange MoveRange =
      CharSourceRange::getCharRange(CallMove->getSourceRange());
  const CharSourceRange FileMoveRange =
      Lexer::makeFileCharRange(MoveRange, SM, getLangOpts());
  if (!FileMoveRange.isValid())
    return;

  const bool IsConstArg = ArgType.isConstQualified();
  const bool IsTriviallyCopyable =
      ArgType.isTriviallyCopyableType(*Result.Context);

  if (IsConstArg || IsTriviallyCopyable) {
    if (const CXXRecordDecl *R = ArgType->getAsCXXRecordDecl()) {
      // According to [expr.prim.lambda]p3, "whether the closure type is
      // trivially copyable" property can be changed by the implementation of
      // the language, so we shouldn't rely on it when issuing diagnostics.
      if (R->isLambda())
        return;
      // Don't warn when the type is not copyable.
      for (const auto *Ctor : R->ctors())
        if (Ctor->isCopyConstructor() &&
            (Ctor->isDeleted() || Ctor->getAccess() != AS_public))
          return;
    }

    if (!IsConstArg && IsTriviallyCopyable && !CheckTriviallyCopyableMove)
      return;

    const bool IsVariable = isa<DeclRefExpr>(Arg);
    // std::move shouldn't be removed when an lvalue wrapped by std::move is
    // passed to the function with an rvalue reference parameter.
    const bool IsRVRefParam =
        isRValueReferenceParam(ReceivingExpr, InvocationParmType, Arg);
    const auto *Var =
        IsVariable ? dyn_cast<DeclRefExpr>(Arg)->getDecl() : nullptr;

    {
      auto Diag = diag(FileMoveRange.getBegin(),
                       "std::move of the %select{|const }0"
                       "%select{expression|variable %5}1 "
                       "%select{|of the trivially-copyable type %6 }2"
                       "has no effect%select{; remove std::move()|}3"
                       "%select{| or make the variable non-const}4")
                  << IsConstArg << IsVariable << IsTriviallyCopyable
                  << IsRVRefParam
                  << (IsConstArg && IsVariable && !IsTriviallyCopyable) << Var
                  << Arg->getType();
      if (!IsRVRefParam)
        replaceCallWithArg(CallMove, Diag, SM, getLangOpts());
    }
    if (IsRVRefParam) {
      // Generate notes for an invocation with an rvalue reference parameter.
      const auto *ReceivingCallExpr = dyn_cast<CallExpr>(ReceivingExpr);
      const auto *ReceivingConstructExpr =
          dyn_cast<CXXConstructExpr>(ReceivingExpr);
      // Skipping the invocation which is a template instantiation.
      if ((!ReceivingCallExpr || !ReceivingCallExpr->getDirectCallee() ||
           ReceivingCallExpr->getDirectCallee()->isTemplateInstantiation()) &&
          (!ReceivingConstructExpr ||
           !ReceivingConstructExpr->getConstructor() ||
           ReceivingConstructExpr->getConstructor()->isTemplateInstantiation()))
        return;

      const NamedDecl *FunctionName = nullptr;
      FunctionName =
          ReceivingCallExpr
              ? ReceivingCallExpr->getDirectCallee()->getUnderlyingDecl()
              : ReceivingConstructExpr->getConstructor()->getUnderlyingDecl();

      QualType NoRefType = (*InvocationParmType)->getPointeeType();
      PrintingPolicy PolicyWithSuppressedTag(getLangOpts());
      PolicyWithSuppressedTag.SuppressTagKeyword = true;
      PolicyWithSuppressedTag.SuppressUnwrittenScope = true;
      std::string ExpectParmTypeName =
          NoRefType.getAsString(PolicyWithSuppressedTag);
      if (!NoRefType->isPointerType()) {
        NoRefType.addConst();
        ExpectParmTypeName =
            NoRefType.getAsString(PolicyWithSuppressedTag) + " &";
      }

      diag(InvocationParm->getLocation(),
           "consider changing the %ordinal0 parameter of %1 from %2 to '%3'",
           DiagnosticIDs::Note)
          << (InvocationParm->getFunctionScopeIndex() + 1) << FunctionName
          << *InvocationParmType << ExpectParmTypeName;
    }
  } else if (ReceivingExpr && CheckMoveToConstRef) {
    if ((*InvocationParmType)->isRValueReferenceType())
      return;

    {
      auto Diag = diag(FileMoveRange.getBegin(),
                       "passing result of std::move() as a const reference "
                       "argument; no move will actually happen");

      replaceCallWithArg(CallMove, Diag, SM, getLangOpts());
    }

    if (const CXXRecordDecl *RecordDecl = ArgType->getAsCXXRecordDecl();
        RecordDecl && RecordDecl->hasDefinition() &&
        !(RecordDecl->hasMoveConstructor() &&
          RecordDecl->hasMoveAssignment())) {
      const bool MissingMoveAssignment = !RecordDecl->hasMoveAssignment();
      const bool MissingMoveConstructor = !RecordDecl->hasMoveConstructor();
      const bool MissingBoth = MissingMoveAssignment && MissingMoveConstructor;

      diag(RecordDecl->getLocation(),
           "%0 is not move "
           "%select{|assignable}1%select{|/}2%select{|constructible}3",
           DiagnosticIDs::Note)
          << RecordDecl << MissingMoveAssignment << MissingBoth
          << MissingMoveConstructor;
    }
  }
}

} // namespace clang::tidy::performance
