//===- unittests/StaticAnalyzer/CallDescriptionTest.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
//
//===----------------------------------------------------------------------===//

#include "Reusables.h"

#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/Tooling/Tooling.h"
#include "gtest/gtest.h"

namespace clang {
namespace ento {
namespace {

// A wrapper around CallDescriptionMap<bool> that allows verifying that
// all functions have been found. This is needed because CallDescriptionMap
// isn't supposed to support iteration.
class ResultMap {
  size_t Found, Total;
  CallDescriptionMap<bool> Impl;

public:
  ResultMap(std::initializer_list<std::pair<CallDescription, bool>> Data)
      : Found(0),
        Total(std::count_if(Data.begin(), Data.end(),
                            [](const std::pair<CallDescription, bool> &Pair) {
                              return Pair.second == true;
                            })),
        Impl(std::move(Data)) {}

  const bool *lookup(const CallEvent &Call) {
    const bool *Result = Impl.lookup(Call);
    // If it's a function we expected to find, remember that we've found it.
    if (Result && *Result)
      ++Found;
    return Result;
  }

  // Fail the test if we haven't found all the true-calls we were looking for.
  ~ResultMap() { EXPECT_EQ(Found, Total); }
};

// Scan the code body for call expressions and see if we find all calls that
// we were supposed to find ("true" in the provided ResultMap) and that we
// don't find the ones that we weren't supposed to find
// ("false" in the ResultMap).
class CallDescriptionConsumer : public ExprEngineConsumer {
  ResultMap &RM;
  void performTest(const Decl *D) {
    using namespace ast_matchers;

    if (!D->hasBody())
      return;

    const CallExpr *CE = findNode<CallExpr>(D, callExpr());
    const StackFrameContext *SFC =
        Eng.getAnalysisDeclContextManager().getStackFrame(D);
    ProgramStateRef State = Eng.getInitialState(SFC);
    CallEventRef<> Call =
        Eng.getStateManager().getCallEventManager().getCall(CE, State, SFC);

    const bool *LookupResult = RM.lookup(*Call);
    // Check that we've found the function in the map
    // with the correct description.
    EXPECT_TRUE(LookupResult && *LookupResult);

    // ResultMap is responsible for making sure that we've found *all* calls.
  }

public:
  CallDescriptionConsumer(CompilerInstance &C,
                          ResultMap &RM)
      : ExprEngineConsumer(C), RM(RM) {}

  bool HandleTopLevelDecl(DeclGroupRef DG) override {
    for (const auto *D : DG)
      performTest(D);
    return true;
  }
};

class CallDescriptionAction : public ASTFrontendAction {
  ResultMap RM;

public:
  CallDescriptionAction(
      std::initializer_list<std::pair<CallDescription, bool>> Data)
      : RM(Data) {}

  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
                                                 StringRef File) override {
    return std::make_unique<CallDescriptionConsumer>(Compiler, RM);
  }
};

TEST(CallEvent, CallDescription) {
  // Test simple name matching.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{"bar"}, false}, // false: there's no call to 'bar' in this code.
          {{"foo"}, true},  // true: there's a call to 'foo' in this code.
      })), "void foo(); void bar() { foo(); }"));

  // Test arguments check.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{"foo", 1}, true},
          {{"foo", 2}, false},
      })), "void foo(int); void foo(int, int); void bar() { foo(1); }"));

  // Test lack of arguments check.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{"foo", None}, true},
          {{"foo", 2}, false},
      })), "void foo(int); void foo(int, int); void bar() { foo(1); }"));

  // Test qualified names.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{{"std", "basic_string", "c_str"}}, true},
      })),
      "namespace std { inline namespace __1 {"
      "  template<typename T> class basic_string {"
      "  public:"
      "    T *c_str();"
      "  };"
      "}}"
      "void foo() {"
      "  using namespace std;"
      "  basic_string<char> s;"
      "  s.c_str();"
      "}"));

  // A negative test for qualified names.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{{"foo", "bar"}}, false},
          {{{"bar", "foo"}}, false},
          {{"foo"}, true},
      })), "void foo(); struct bar { void foo(); }; void test() { foo(); }"));

  // Test CDF_MaybeBuiltin - a flag that allows matching weird builtins.
  EXPECT_TRUE(tooling::runToolOnCode(
      std::unique_ptr<CallDescriptionAction>(new CallDescriptionAction({
          {{"memset", 3}, false},
          {{CDF_MaybeBuiltin, "memset", 3}, true}
      })),
      "void foo() {"
      "  int x;"
      "  __builtin___memset_chk(&x, 0, sizeof(x),"
      "                         __builtin_object_size(&x, 0));"
      "}"));
}

} // namespace
} // namespace ento
} // namespace clang
