//===- ChromiumCheckModelTest.cpp -----------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
// FIXME: Move this to clang/unittests/Analysis/FlowSensitive/Models.

#include "clang/Analysis/FlowSensitive/Models/ChromiumCheckModel.h"
#include "TestingSupport.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/CFG.h"
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <string>

using namespace clang;
using namespace dataflow;
using namespace test;

namespace {
using ::testing::NotNull;
using ::testing::UnorderedElementsAre;

static constexpr char ChromiumCheckHeader[] = R"(
namespace std {
class ostream;
} // namespace std

namespace logging {
class VoidifyStream {
 public:
  VoidifyStream() = default;
  void operator&(std::ostream&) {}
};

class CheckError {
 public:
  static CheckError Check(const char* file, int line, const char* condition);
  static CheckError DCheck(const char* file, int line, const char* condition);
  static CheckError PCheck(const char* file, int line, const char* condition);
  static CheckError PCheck(const char* file, int line);
  static CheckError DPCheck(const char* file, int line, const char* condition);

  std::ostream& stream();

  ~CheckError();

  CheckError(const CheckError& other) = delete;
  CheckError& operator=(const CheckError& other) = delete;
  CheckError(CheckError&& other) = default;
  CheckError& operator=(CheckError&& other) = default;
};

} // namespace logging

#define LAZY_CHECK_STREAM(stream, condition) \
  !(condition) ? (void)0 : ::logging::VoidifyStream() & (stream)

#define CHECK(condition)                                                     \
  LAZY_CHECK_STREAM(                                                         \
      ::logging::CheckError::Check(__FILE__, __LINE__, #condition).stream(), \
      !(condition))

#define PCHECK(condition)                                                     \
  LAZY_CHECK_STREAM(                                                          \
      ::logging::CheckError::PCheck(__FILE__, __LINE__, #condition).stream(), \
      !(condition))

#define DCHECK(condition)                                                     \
  LAZY_CHECK_STREAM(                                                          \
      ::logging::CheckError::DCheck(__FILE__, __LINE__, #condition).stream(), \
      !(condition))

#define DPCHECK(condition)                                                     \
  LAZY_CHECK_STREAM(                                                           \
      ::logging::CheckError::DPCheck(__FILE__, __LINE__, #condition).stream(), \
      !(condition))
)";

// A definition of the `CheckError` class that looks like the Chromium one, but
// is actually something else.
static constexpr char OtherCheckHeader[] = R"(
namespace other {
namespace logging {
class CheckError {
 public:
  static CheckError Check(const char* file, int line, const char* condition);
};
} // namespace logging
} // namespace other
)";

/// Replaces all occurrences of `Pattern` in `S` with `Replacement`.
std::string ReplacePattern(std::string S, const std::string &Pattern,
                           const std::string &Replacement) {
  size_t Pos = 0;
  Pos = S.find(Pattern, Pos);
  if (Pos != std::string::npos)
    S.replace(Pos, Pattern.size(), Replacement);
  return S;
}

template <typename Model>
class ModelAdaptorAnalysis
    : public DataflowAnalysis<ModelAdaptorAnalysis<Model>, NoopLattice> {
public:
  explicit ModelAdaptorAnalysis(ASTContext &Context)
      : DataflowAnalysis<ModelAdaptorAnalysis, NoopLattice>(
            Context, /*ApplyBuiltinTransfer=*/true) {}

  static NoopLattice initialElement() { return NoopLattice(); }

  void transfer(const CFGElement *E, NoopLattice &, Environment &Env) {
    M.transfer(E, Env);
  }

private:
  Model M;
};

template <typename Matcher>
void runDataflow(llvm::StringRef Code, Matcher Match) {
  const tooling::FileContentMappings FileContents = {
      {"check.h", ChromiumCheckHeader}, {"othercheck.h", OtherCheckHeader}};

  ASSERT_THAT_ERROR(
      checkDataflow<ModelAdaptorAnalysis<ChromiumCheckModel>>(
          AnalysisInputs<ModelAdaptorAnalysis<ChromiumCheckModel>>(
              Code, ast_matchers::hasName("target"),
              [](ASTContext &C, Environment &) {
                return ModelAdaptorAnalysis<ChromiumCheckModel>(C);
              })
              .withASTBuildArgs({"-fsyntax-only",
                                 "-fno-delayed-template-parsing", "-std=c++17"})
              .withASTBuildVirtualMappedFiles(std::move(FileContents)),
          /*VerifyResults=*/
          [&Match](const llvm::StringMap<DataflowAnalysisState<NoopLattice>>
                       &Results,
                   const AnalysisOutputs &AO) { Match(Results, AO.ASTCtx); }),
      llvm::Succeeded());
}

TEST(ChromiumCheckModelTest, CheckSuccessImpliesConditionHolds) {
  auto Expectations =
      [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
         ASTContext &ASTCtx) {
        ASSERT_THAT(Results.keys(), UnorderedElementsAre("p"));
        const Environment &Env = getEnvironmentAtAnnotation(Results, "p");

        const ValueDecl *FooDecl = findValueDecl(ASTCtx, "Foo");
        ASSERT_THAT(FooDecl, NotNull());

        auto *FooVal = cast<BoolValue>(Env.getValue(*FooDecl, SkipPast::None));

        EXPECT_TRUE(Env.flowConditionImplies(*FooVal));
      };

  std::string Code = R"(
    #include "check.h"

    void target(bool Foo) {
      $check(Foo);
      bool X = true;
      (void)X;
      // [[p]]
    }
  )";
  runDataflow(ReplacePattern(Code, "$check", "CHECK"), Expectations);
  runDataflow(ReplacePattern(Code, "$check", "DCHECK"), Expectations);
  runDataflow(ReplacePattern(Code, "$check", "PCHECK"), Expectations);
  runDataflow(ReplacePattern(Code, "$check", "DPCHECK"), Expectations);
}

TEST(ChromiumCheckModelTest, UnrelatedCheckIgnored) {
  auto Expectations =
      [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
         ASTContext &ASTCtx) {
        ASSERT_THAT(Results.keys(), UnorderedElementsAre("p"));
        const Environment &Env = getEnvironmentAtAnnotation(Results, "p");

        const ValueDecl *FooDecl = findValueDecl(ASTCtx, "Foo");
        ASSERT_THAT(FooDecl, NotNull());

        auto *FooVal = cast<BoolValue>(Env.getValue(*FooDecl, SkipPast::None));

        EXPECT_FALSE(Env.flowConditionImplies(*FooVal));
      };

  std::string Code = R"(
    #include "othercheck.h"

    void target(bool Foo) {
      if (!Foo) {
        (void)other::logging::CheckError::Check(__FILE__, __LINE__, "Foo");
      }
      bool X = true;
      (void)X;
      // [[p]]
    }
  )";
  runDataflow(Code, Expectations);
}
} // namespace
