//===----------------------------------------------------------------------===//
//
// 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 "PreferIsaOrDynCastInConditionalsCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"
#include "llvm/Support/FormatVariadic.h"

using namespace clang::ast_matchers;

namespace clang::tidy::llvm_check {

namespace {
AST_MATCHER(Expr, isMacroID) { return Node.getExprLoc().isMacroID(); }
} // namespace

void PreferIsaOrDynCastInConditionalsCheck::registerMatchers(
    MatchFinder *Finder) {
  auto AnyCalleeName = [](ArrayRef<StringRef> CalleeName) {
    return allOf(unless(isMacroID()), unless(cxxMemberCallExpr()),
                 callee(expr(ignoringImpCasts(
                     declRefExpr(to(namedDecl(hasAnyName(CalleeName))),
                                 hasAnyTemplateArgumentLoc(anything()))
                         .bind("callee")))));
  };

  auto CondExpr = hasCondition(implicitCastExpr(
      has(callExpr(AnyCalleeName({"cast", "dyn_cast"})).bind("cond"))));

  auto CondExprOrCondVar =
      anyOf(hasConditionVariableStatement(containsDeclaration(
                0, varDecl(hasInitializer(callExpr(AnyCalleeName({"cast"}))))
                       .bind("var"))),
            CondExpr);

  auto CallWithBindedArg =
      callExpr(
          AnyCalleeName(
              {"isa", "cast", "cast_or_null", "dyn_cast", "dyn_cast_or_null"}),
          hasArgument(0, mapAnyOf(declRefExpr, cxxMemberCallExpr).bind("arg")))
          .bind("rhs");

  Finder->addMatcher(
      stmt(anyOf(ifStmt(CondExprOrCondVar), forStmt(CondExprOrCondVar),
                 whileStmt(CondExprOrCondVar), doStmt(CondExpr),
                 binaryOperator(unless(isExpansionInFileMatching(
                                    "llvm/include/llvm/Support/Casting.h")),
                                hasOperatorName("&&"),
                                hasLHS(implicitCastExpr().bind("lhs")),
                                hasRHS(ignoringImpCasts(CallWithBindedArg)))
                     .bind("and"))),
      this);
}

void PreferIsaOrDynCastInConditionalsCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *Callee = Result.Nodes.getNodeAs<DeclRefExpr>("callee");

  assert(Callee && "Callee should be binded if anything is matched");

  // The first and last letter of the identifier
  //   llvm::cast<T>(x)
  //         ^  ^
  //  StartLoc  EndLoc
  const SourceLocation StartLoc = Callee->getLocation();
  const SourceLocation EndLoc = Callee->getNameInfo().getEndLoc();

  if (Result.Nodes.getNodeAs<VarDecl>("var")) {
    diag(StartLoc,
         "cast<> in conditional will assert rather than return a null pointer")
        << FixItHint::CreateReplacement(SourceRange(StartLoc, EndLoc),
                                        "dyn_cast");
  } else if (Result.Nodes.getNodeAs<CallExpr>("cond")) {
    StringRef Message =
        "cast<> in conditional will assert rather than return a null pointer";
    if (Callee->getDecl()->getName() == "dyn_cast")
      Message = "return value from dyn_cast<> not used";

    diag(StartLoc, Message)
        << FixItHint::CreateReplacement(SourceRange(StartLoc, EndLoc), "isa");
  } else if (Result.Nodes.getNodeAs<BinaryOperator>("and")) {
    const auto *LHS = Result.Nodes.getNodeAs<ImplicitCastExpr>("lhs");
    const auto *RHS = Result.Nodes.getNodeAs<CallExpr>("rhs");
    const auto *Arg = Result.Nodes.getNodeAs<Expr>("arg");

    assert(LHS && "LHS is null");
    assert(RHS && "RHS is null");
    assert(Arg && "Arg is null");

    auto GetText = [&](SourceRange R) {
      return Lexer::getSourceText(CharSourceRange::getTokenRange(R),
                                  *Result.SourceManager, getLangOpts());
    };

    const StringRef LHSString = GetText(LHS->getSourceRange());
    const StringRef ArgString = GetText(Arg->getSourceRange());

    if (ArgString != LHSString)
      return;

    // It is not clear which is preferred between `isa_and_nonnull` and
    // `isa_and_present`. See
    // https://discourse.llvm.org/t/psa-swapping-out-or-null-with-if-present/65018
    const std::string Replacement = llvm::formatv(
        "{}isa_and_nonnull{}",
        GetText(Callee->getQualifierLoc().getSourceRange()),
        GetText(SourceRange(Callee->getLAngleLoc(), RHS->getEndLoc())));

    diag(LHS->getBeginLoc(),
         "isa_and_nonnull<> is preferred over an explicit test for null "
         "followed by calling isa<>")
        << FixItHint::CreateReplacement(
               SourceRange(LHS->getBeginLoc(), RHS->getEndLoc()), Replacement);
  } else {
    llvm_unreachable(
        R"(One of "var", "cond" and "and" should be binded if anything is matched)");
  }
}

} // namespace clang::tidy::llvm_check
