//===---- TransformerClangTidyCheckTest.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 "../clang-tidy/utils/TransformerClangTidyCheck.h"
#include "ClangTidyTest.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Tooling/Transformer/RangeSelector.h"
#include "clang/Tooling/Transformer/RewriteRule.h"
#include "clang/Tooling/Transformer/Stencil.h"
#include "clang/Tooling/Transformer/Transformer.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <optional>

namespace clang {
namespace tidy {
namespace utils {
namespace {
using namespace ::clang::ast_matchers;

using transformer::cat;
using transformer::change;
using transformer::IncludeFormat;
using transformer::makeRule;
using transformer::node;
using transformer::noopEdit;
using transformer::note;
using transformer::RewriteRuleWith;
using transformer::RootID;
using transformer::statement;

// Invert the code of an if-statement, while maintaining its semantics.
RewriteRuleWith<std::string> invertIf() {
  StringRef C = "C", T = "T", E = "E";
  RewriteRuleWith<std::string> Rule = makeRule(
      ifStmt(hasCondition(expr().bind(C)), hasThen(stmt().bind(T)),
             hasElse(stmt().bind(E))),
      change(statement(RootID), cat("if(!(", node(std::string(C)), ")) ",
                                    statement(std::string(E)), " else ",
                                    statement(std::string(T)))),
      cat("negate condition and reverse `then` and `else` branches"));
  return Rule;
}

class IfInverterCheck : public TransformerClangTidyCheck {
public:
  IfInverterCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(invertIf(), Name, Context) {}
};

// Basic test of using a rewrite rule as a ClangTidy.
TEST(TransformerClangTidyCheckTest, Basic) {
  const std::string Input = R"cc(
    void log(const char* msg);
    void foo() {
      if (10 > 1.0)
        log("oh no!");
      else
        log("ok");
    }
  )cc";
  const std::string Expected = R"(
    void log(const char* msg);
    void foo() {
      if(!(10 > 1.0)) log("ok"); else log("oh no!");
    }
  )";
  EXPECT_EQ(Expected, test::runCheckOnCode<IfInverterCheck>(Input));
}

TEST(TransformerClangTidyCheckTest, DiagnosticsCorrectlyGenerated) {
  class DiagOnlyCheck : public TransformerClangTidyCheck {
  public:
    DiagOnlyCheck(StringRef Name, ClangTidyContext *Context)
        : TransformerClangTidyCheck(
              makeRule(returnStmt(), noopEdit(node(RootID)), cat("message")),
              Name, Context) {}
  };
  std::string Input = "int h() { return 5; }";
  std::vector<ClangTidyError> Errors;
  EXPECT_EQ(Input, test::runCheckOnCode<DiagOnlyCheck>(Input, &Errors));
  EXPECT_EQ(Errors.size(), 1U);
  EXPECT_EQ(Errors[0].Message.Message, "message");
  EXPECT_THAT(Errors[0].Message.Ranges, testing::IsEmpty());
  EXPECT_THAT(Errors[0].Notes, testing::IsEmpty());

  // The diagnostic is anchored to the match, "return 5".
  EXPECT_EQ(Errors[0].Message.FileOffset, 10U);
}

transformer::ASTEdit noReplacementEdit(transformer::RangeSelector Target) {
  transformer::ASTEdit E;
  E.TargetRange = std::move(Target);
  return E;
}

TEST(TransformerClangTidyCheckTest, EmptyReplacement) {
  class DiagOnlyCheck : public TransformerClangTidyCheck {
  public:
    DiagOnlyCheck(StringRef Name, ClangTidyContext *Context)
        : TransformerClangTidyCheck(
              makeRule(returnStmt(), edit(noReplacementEdit(node(RootID))),
                       cat("message")),
              Name, Context) {}
  };
  std::string Input = "int h() { return 5; }";
  std::vector<ClangTidyError> Errors;
  EXPECT_EQ("int h() { }", test::runCheckOnCode<DiagOnlyCheck>(Input, &Errors));
  EXPECT_EQ(Errors.size(), 1U);
  EXPECT_EQ(Errors[0].Message.Message, "message");
  EXPECT_THAT(Errors[0].Message.Ranges, testing::IsEmpty());

  // The diagnostic is anchored to the match, "return 5".
  EXPECT_EQ(Errors[0].Message.FileOffset, 10U);
}

TEST(TransformerClangTidyCheckTest, NotesCorrectlyGenerated) {
  class DiagAndNoteCheck : public TransformerClangTidyCheck {
  public:
    DiagAndNoteCheck(StringRef Name, ClangTidyContext *Context)
        : TransformerClangTidyCheck(
              makeRule(returnStmt(),
                       note(node(RootID), cat("some note")),
                       cat("message")),
              Name, Context) {}
  };
  std::string Input = "int h() { return 5; }";
  std::vector<ClangTidyError> Errors;
  EXPECT_EQ(Input, test::runCheckOnCode<DiagAndNoteCheck>(Input, &Errors));
  EXPECT_EQ(Errors.size(), 1U);
  EXPECT_EQ(Errors[0].Notes.size(), 1U);
  EXPECT_EQ(Errors[0].Notes[0].Message, "some note");

  // The note is anchored to the match, "return 5".
  EXPECT_EQ(Errors[0].Notes[0].FileOffset, 10U);
}

TEST(TransformerClangTidyCheckTest, DiagnosticMessageEscaped) {
  class GiveDiagWithPercentSymbol : public TransformerClangTidyCheck {
  public:
    GiveDiagWithPercentSymbol(StringRef Name, ClangTidyContext *Context)
        : TransformerClangTidyCheck(makeRule(returnStmt(),
                                             noopEdit(node(RootID)),
                                             cat("bad code: x % y % z")),
                                    Name, Context) {}
  };
  std::string Input = "int somecode() { return 0; }";
  std::vector<ClangTidyError> Errors;
  EXPECT_EQ(Input,
            test::runCheckOnCode<GiveDiagWithPercentSymbol>(Input, &Errors));
  ASSERT_EQ(Errors.size(), 1U);
  // The message stored in this field shouldn't include escaped percent signs,
  // because the diagnostic printer should have _unescaped_ them when processing
  // the diagnostic. The only behavior observable/verifiable by the test is that
  // the presence of the '%' doesn't crash Clang.
  EXPECT_EQ(Errors[0].Message.Message, "bad code: x % y % z");
}

class IntLitCheck : public TransformerClangTidyCheck {
public:
  IntLitCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(
            makeRule(integerLiteral(), change(cat("LIT")), cat("no message")),
            Name, Context) {}
};

// Tests that two changes in a single macro expansion do not lead to conflicts
// in applying the changes.
TEST(TransformerClangTidyCheckTest, TwoChangesInOneMacroExpansion) {
  const std::string Input = R"cc(
#define PLUS(a,b) (a) + (b)
    int f() { return PLUS(3, 4); }
  )cc";
  const std::string Expected = R"cc(
#define PLUS(a,b) (a) + (b)
    int f() { return PLUS(LIT, LIT); }
  )cc";

  EXPECT_EQ(Expected, test::runCheckOnCode<IntLitCheck>(Input));
}

class BinOpCheck : public TransformerClangTidyCheck {
public:
  BinOpCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(
            makeRule(
                binaryOperator(hasOperatorName("+"), hasRHS(expr().bind("r"))),
                change(node("r"), cat("RIGHT")), cat("no message")),
            Name, Context) {}
};

// Tests case where the rule's match spans both source from the macro and its
// argument, while the change spans only the argument AND there are two such
// matches. We verify that both replacements succeed.
TEST(TransformerClangTidyCheckTest, TwoMatchesInMacroExpansion) {
  const std::string Input = R"cc(
#define M(a,b) (1 + a) * (1 + b)
    int f() { return M(3, 4); }
  )cc";
  const std::string Expected = R"cc(
#define M(a,b) (1 + a) * (1 + b)
    int f() { return M(RIGHT, RIGHT); }
  )cc";

  EXPECT_EQ(Expected, test::runCheckOnCode<BinOpCheck>(Input));
}

// A trivial rewrite-rule generator that requires Objective-C code.
std::optional<RewriteRuleWith<std::string>>
needsObjC(const LangOptions &LangOpts,
          const ClangTidyCheck::OptionsView &Options) {
  if (!LangOpts.ObjC)
    return std::nullopt;
  return makeRule(clang::ast_matchers::functionDecl(),
                  change(cat("void changed() {}")), cat("no message"));
}

class NeedsObjCCheck : public TransformerClangTidyCheck {
public:
  NeedsObjCCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(needsObjC, Name, Context) {}
};

// Verify that the check only rewrites the code when the input is Objective-C.
TEST(TransformerClangTidyCheckTest, DisableByLang) {
  const std::string Input = "void log() {}";
  EXPECT_EQ(Input,
            test::runCheckOnCode<NeedsObjCCheck>(Input, nullptr, "input.cc"));

  EXPECT_EQ("void changed() {}",
            test::runCheckOnCode<NeedsObjCCheck>(Input, nullptr, "input.mm"));
}

// A trivial rewrite rule generator that checks config options.
std::optional<RewriteRuleWith<std::string>>
noSkip(const LangOptions &LangOpts,
       const ClangTidyCheck::OptionsView &Options) {
  if (Options.get("Skip", "false") == "true")
    return std::nullopt;
  return makeRule(clang::ast_matchers::functionDecl(),
                  changeTo(cat("void nothing();")), cat("no message"));
}

class ConfigurableCheck : public TransformerClangTidyCheck {
public:
  ConfigurableCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(noSkip, Name, Context) {}
};

// Tests operation with config option "Skip" set to true and false.
TEST(TransformerClangTidyCheckTest, DisableByConfig) {
  const std::string Input = "void log(int);";
  const std::string Expected = "void nothing();";
  ClangTidyOptions Options;

  Options.CheckOptions["test-check-0.Skip"] = "true";
  EXPECT_EQ(Input, test::runCheckOnCode<ConfigurableCheck>(
                       Input, nullptr, "input.cc", {}, Options));

  Options.CheckOptions["test-check-0.Skip"] = "false";
  EXPECT_EQ(Expected, test::runCheckOnCode<ConfigurableCheck>(
                          Input, nullptr, "input.cc", {}, Options));
}

RewriteRuleWith<std::string> replaceCall(IncludeFormat Format) {
  using namespace ::clang::ast_matchers;
  RewriteRuleWith<std::string> Rule =
      makeRule(callExpr(callee(functionDecl(hasName("f")))),
               change(cat("other()")), cat("no message"));
  addInclude(Rule, "clang/OtherLib.h", Format);
  return Rule;
}

template <IncludeFormat Format>
class IncludeCheck : public TransformerClangTidyCheck {
public:
  IncludeCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(replaceCall(Format), Name, Context) {}
};

TEST(TransformerClangTidyCheckTest, AddIncludeQuoted) {

  std::string Input = R"cc(
    int f(int x);
    int h(int x) { return f(x); }
  )cc";
  std::string Expected = R"cc(#include "clang/OtherLib.h"


    int f(int x);
    int h(int x) { return other(); }
  )cc";

  EXPECT_EQ(Expected,
            test::runCheckOnCode<IncludeCheck<IncludeFormat::Quoted>>(Input));
}

TEST(TransformerClangTidyCheckTest, AddIncludeAngled) {
  std::string Input = R"cc(
    int f(int x);
    int h(int x) { return f(x); }
  )cc";
  std::string Expected = R"cc(#include <clang/OtherLib.h>


    int f(int x);
    int h(int x) { return other(); }
  )cc";

  EXPECT_EQ(Expected,
            test::runCheckOnCode<IncludeCheck<IncludeFormat::Angled>>(Input));
}

class IncludeOrderCheck : public TransformerClangTidyCheck {
  static RewriteRuleWith<std::string> rule() {
    using namespace ::clang::ast_matchers;
    RewriteRuleWith<std::string> Rule = transformer::makeRule(
        integerLiteral(), change(cat("5")), cat("no message"));
    addInclude(Rule, "bar.h", IncludeFormat::Quoted);
    return Rule;
  }

public:
  IncludeOrderCheck(StringRef Name, ClangTidyContext *Context)
      : TransformerClangTidyCheck(rule(), Name, Context) {}
};

TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleLocalOption) {
  std::string Input = R"cc(#include "input.h"
int h(int x) { return 3; })cc";

  std::string TreatsAsLibraryHeader = R"cc(#include "input.h"

#include "bar.h"
int h(int x) { return 5; })cc";

  std::string TreatsAsNormalHeader = R"cc(#include "bar.h"
#include "input.h"
int h(int x) { return 5; })cc";

  ClangTidyOptions Options;
  std::map<StringRef, StringRef> PathsToContent = {{"input.h", "\n"}};
  Options.CheckOptions["test-check-0.IncludeStyle"] = "llvm";
  EXPECT_EQ(TreatsAsLibraryHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "inputTest.cpp", {}, Options, PathsToContent));
  EXPECT_EQ(TreatsAsNormalHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "input_test.cpp", {}, Options, PathsToContent));

  Options.CheckOptions["test-check-0.IncludeStyle"] = "google";
  EXPECT_EQ(TreatsAsNormalHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "inputTest.cc", {}, Options, PathsToContent));
  EXPECT_EQ(TreatsAsLibraryHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "input_test.cc", {}, Options, PathsToContent));
}

TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleGlobalOption) {
  std::string Input = R"cc(#include "input.h"
int h(int x) { return 3; })cc";

  std::string TreatsAsLibraryHeader = R"cc(#include "input.h"

#include "bar.h"
int h(int x) { return 5; })cc";

  std::string TreatsAsNormalHeader = R"cc(#include "bar.h"
#include "input.h"
int h(int x) { return 5; })cc";

  ClangTidyOptions Options;
  std::map<StringRef, StringRef> PathsToContent = {{"input.h", "\n"}};
  Options.CheckOptions["IncludeStyle"] = "llvm";
  EXPECT_EQ(TreatsAsLibraryHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "inputTest.cpp", {}, Options, PathsToContent));
  EXPECT_EQ(TreatsAsNormalHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "input_test.cpp", {}, Options, PathsToContent));

  Options.CheckOptions["IncludeStyle"] = "google";
  EXPECT_EQ(TreatsAsNormalHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "inputTest.cc", {}, Options, PathsToContent));
  EXPECT_EQ(TreatsAsLibraryHeader,
            test::runCheckOnCode<IncludeOrderCheck>(
                Input, nullptr, "input_test.cc", {}, Options, PathsToContent));
}

} // namespace
} // namespace utils
} // namespace tidy
} // namespace clang
