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

#include "../utils/ASTUtils.h"
#include "../utils/Matchers.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Lex/Lexer.h"

#include <string>

using namespace clang::ast_matchers;

namespace clang::tidy::modernize {

static bool isNegativeComparison(const Expr *ComparisonExpr) {
  if (const auto *Op = llvm::dyn_cast<BinaryOperator>(ComparisonExpr))
    return Op->getOpcode() == BO_NE;

  if (const auto *Op = llvm::dyn_cast<CXXOperatorCallExpr>(ComparisonExpr))
    return Op->getOperator() == OO_ExclaimEqual;

  if (const auto *Op =
          llvm::dyn_cast<CXXRewrittenBinaryOperator>(ComparisonExpr))
    return Op->getOperator() == BO_NE;

  return false;
}

struct NotLengthExprForStringNode {
  NotLengthExprForStringNode(std::string ID, DynTypedNode Node,
                             ASTContext *Context)
      : ID(std::move(ID)), Node(std::move(Node)), Context(Context) {}
  bool operator()(const internal::BoundNodesMap &Nodes) const {
    // Match a string literal and an integer size or strlen() call.
    if (const auto *StringLiteralNode = Nodes.getNodeAs<StringLiteral>(ID)) {
      if (const auto *IntegerLiteralSizeNode = Node.get<IntegerLiteral>()) {
        return StringLiteralNode->getLength() !=
               IntegerLiteralSizeNode->getValue().getZExtValue();
      }

      if (const auto *StrlenNode = Node.get<CallExpr>()) {
        if (StrlenNode->getDirectCallee()->getName() != "strlen" ||
            StrlenNode->getNumArgs() != 1) {
          return true;
        }

        if (const auto *StrlenArgNode = dyn_cast<StringLiteral>(
                StrlenNode->getArg(0)->IgnoreParenImpCasts())) {
          return StrlenArgNode->getLength() != StringLiteralNode->getLength();
        }
      }
    }

    // Match a string variable and a call to length() or size().
    if (const auto *ExprNode = Nodes.getNodeAs<Expr>(ID)) {
      if (const auto *MemberCallNode = Node.get<CXXMemberCallExpr>()) {
        const CXXMethodDecl *MethodDeclNode = MemberCallNode->getMethodDecl();
        const StringRef Name = MethodDeclNode->getName();
        if (!MethodDeclNode->isConst() || MethodDeclNode->getNumParams() != 0 ||
            (Name != "size" && Name != "length")) {
          return true;
        }

        if (const auto *OnNode =
                dyn_cast<Expr>(MemberCallNode->getImplicitObjectArgument())) {
          return !utils::areStatementsIdentical(OnNode->IgnoreParenImpCasts(),
                                                ExprNode->IgnoreParenImpCasts(),
                                                *Context);
        }
      }
    }

    return true;
  }

private:
  std::string ID;
  DynTypedNode Node;
  ASTContext *Context;
};

AST_MATCHER_P(Expr, lengthExprForStringNode, std::string, ID) {
  return Builder->removeBindings(NotLengthExprForStringNode(
      ID, DynTypedNode::create(Node), &(Finder->getASTContext())));
}

UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
                                               ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context) {}

void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
  const auto ZeroLiteral = integerLiteral(equals(0));

  const auto ClassTypeWithMethod = [](const StringRef MethodBoundName,
                                      const auto... Methods) {
    return cxxRecordDecl(anyOf(
        hasMethod(cxxMethodDecl(isConst(), parameterCountIs(1),
                                returns(booleanType()), hasAnyName(Methods))
                      .bind(MethodBoundName))...));
  };

  const auto OnClassWithStartsWithFunction =
      ClassTypeWithMethod("starts_with_fun", "starts_with", "startsWith",
                          "startswith", "StartsWith");

  const auto OnClassWithEndsWithFunction = ClassTypeWithMethod(
      "ends_with_fun", "ends_with", "endsWith", "endswith", "EndsWith");

  // Case 1: X.find(Y) [!=]= 0 -> starts_with.
  const auto FindExpr = cxxMemberCallExpr(
      anyOf(argumentCountIs(1), hasArgument(1, ZeroLiteral)),
      callee(
          cxxMethodDecl(hasName("find"), ofClass(OnClassWithStartsWithFunction))
              .bind("find_fun")),
      hasArgument(0, expr().bind("needle")));

  // Case 2: X.rfind(Y, 0) [!=]= 0 -> starts_with.
  const auto RFindExpr = cxxMemberCallExpr(
      hasArgument(1, ZeroLiteral),
      callee(cxxMethodDecl(hasName("rfind"),
                           ofClass(OnClassWithStartsWithFunction))
                 .bind("find_fun")),
      hasArgument(0, expr().bind("needle")));

  // Case 3: X.compare(0, LEN(Y), Y) [!=]= 0 -> starts_with.
  const auto CompareExpr = cxxMemberCallExpr(
      argumentCountIs(3), hasArgument(0, ZeroLiteral),
      callee(cxxMethodDecl(hasName("compare"),
                           ofClass(OnClassWithStartsWithFunction))
                 .bind("find_fun")),
      hasArgument(2, expr().bind("needle")),
      hasArgument(1, lengthExprForStringNode("needle")));

  // Case 4: X.compare(LEN(X) - LEN(Y), LEN(Y), Y) [!=]= 0 -> ends_with.
  const auto CompareEndsWithExpr = cxxMemberCallExpr(
      argumentCountIs(3),
      callee(cxxMethodDecl(hasName("compare"),
                           ofClass(OnClassWithEndsWithFunction))
                 .bind("find_fun")),
      on(expr().bind("haystack")), hasArgument(2, expr().bind("needle")),
      hasArgument(1, lengthExprForStringNode("needle")),
      hasArgument(0,
                  binaryOperator(hasOperatorName("-"),
                                 hasLHS(lengthExprForStringNode("haystack")),
                                 hasRHS(lengthExprForStringNode("needle")))));

  // All cases comparing to 0.
  Finder->addMatcher(
      binaryOperator(
          matchers::isEqualityOperator(),
          hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr,
                                              CompareEndsWithExpr))
                          .bind("find_expr"),
                      ZeroLiteral))
          .bind("expr"),
      this);

  // Case 5: X.rfind(Y) [!=]= LEN(X) - LEN(Y) -> ends_with.
  Finder->addMatcher(
      binaryOperator(
          matchers::isEqualityOperator(),
          hasOperands(
              cxxMemberCallExpr(
                  anyOf(
                      argumentCountIs(1),
                      allOf(argumentCountIs(2),
                            hasArgument(
                                1,
                                anyOf(declRefExpr(to(varDecl(hasName("npos")))),
                                      memberExpr(member(hasName("npos"))))))),
                  callee(cxxMethodDecl(hasName("rfind"),
                                       ofClass(OnClassWithEndsWithFunction))
                             .bind("find_fun")),
                  on(expr().bind("haystack")),
                  hasArgument(0, expr().bind("needle")))
                  .bind("find_expr"),
              binaryOperator(hasOperatorName("-"),
                             hasLHS(lengthExprForStringNode("haystack")),
                             hasRHS(lengthExprForStringNode("needle")))))
          .bind("expr"),
      this);

  // Case 6: X.substr(0, LEN(Y)) [!=]= Y -> starts_with.
  Finder->addMatcher(
      binaryOperation(
          hasAnyOperatorName("==", "!="),
          hasOperands(
              expr().bind("needle"),
              cxxMemberCallExpr(
                  argumentCountIs(2), hasArgument(0, ZeroLiteral),
                  hasArgument(1, lengthExprForStringNode("needle")),
                  callee(cxxMethodDecl(hasName("substr"),
                                       ofClass(OnClassWithStartsWithFunction))
                             .bind("find_fun")))
                  .bind("find_expr")))
          .bind("expr"),
      this);
}

void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *ComparisonExpr = Result.Nodes.getNodeAs<Expr>("expr");
  const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
  const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
  const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("needle");
  const auto *StartsWithFunction =
      Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
  const auto *EndsWithFunction =
      Result.Nodes.getNodeAs<CXXMethodDecl>("ends_with_fun");
  assert(bool(StartsWithFunction) != bool(EndsWithFunction));

  const CXXMethodDecl *ReplacementFunction =
      StartsWithFunction ? StartsWithFunction : EndsWithFunction;

  if (ComparisonExpr->getBeginLoc().isMacroID() ||
      FindExpr->getBeginLoc().isMacroID())
    return;

  // Make sure FindExpr->getArg(0) can be used to make a range in the FitItHint.
  if (FindExpr->getNumArgs() == 0)
    return;

  // Retrieve the source text of the search expression.
  const auto SearchExprText = Lexer::getSourceText(
      CharSourceRange::getTokenRange(SearchExpr->getSourceRange()),
      *Result.SourceManager, Result.Context->getLangOpts());

  auto Diagnostic = diag(FindExpr->getExprLoc(), "use %0 instead of %1")
                    << ReplacementFunction->getName() << FindFun->getName();

  // Remove everything before the function call.
  Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
      ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));

  // Rename the function to `starts_with` or `ends_with`.
  Diagnostic << FixItHint::CreateReplacement(FindExpr->getExprLoc(),
                                             ReplacementFunction->getName());

  // Replace arguments and everything after the function call.
  Diagnostic << FixItHint::CreateReplacement(
      CharSourceRange::getTokenRange(FindExpr->getArg(0)->getBeginLoc(),
                                     ComparisonExpr->getEndLoc()),
      (SearchExprText + ")").str());

  // Add negation if necessary.
  if (isNegativeComparison(ComparisonExpr))
    Diagnostic << FixItHint::CreateInsertion(FindExpr->getBeginLoc(), "!");
}

} // namespace clang::tidy::modernize
