//===- unittest/Tooling/CrossTranslationUnitTest.cpp - Tooling unit tests -===//
//
// 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/CrossTU/CrossTranslationUnit.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/ToolOutputFile.h"
#include "gtest/gtest.h"
#include <cassert>

namespace clang {
namespace cross_tu {

namespace {

class CTUASTConsumer : public clang::ASTConsumer {
public:
  explicit CTUASTConsumer(clang::CompilerInstance &CI, bool *Success)
      : CTU(CI), Success(Success) {}

  void HandleTranslationUnit(ASTContext &Ctx) {
    auto FindFInTU = [](const TranslationUnitDecl *TU) {
      const FunctionDecl *FD = nullptr;
      for (const Decl *D : TU->decls()) {
        FD = dyn_cast<FunctionDecl>(D);
        if (FD && FD->getName() == "f")
          break;
      }
      return FD;
    };

    const TranslationUnitDecl *TU = Ctx.getTranslationUnitDecl();
    const FunctionDecl *FD = FindFInTU(TU);
    assert(FD && FD->getName() == "f");
    bool OrigFDHasBody = FD->hasBody();

    // Prepare the index file and the AST file.
    int ASTFD;
    llvm::SmallString<256> ASTFileName;
    ASSERT_FALSE(
        llvm::sys::fs::createTemporaryFile("f_ast", "ast", ASTFD, ASTFileName));
    llvm::ToolOutputFile ASTFile(ASTFileName, ASTFD);

    int IndexFD;
    llvm::SmallString<256> IndexFileName;
    ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("index", "txt", IndexFD,
                                                    IndexFileName));
    llvm::ToolOutputFile IndexFile(IndexFileName, IndexFD);
    IndexFile.os() << "c:@F@f#I# " << ASTFileName << "\n";
    IndexFile.os().flush();
    EXPECT_TRUE(llvm::sys::fs::exists(IndexFileName));

    StringRef SourceText = "int f(int) { return 0; }\n";
    // This file must exist since the saved ASTFile will reference it.
    int SourceFD;
    llvm::SmallString<256> SourceFileName;
    ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("input", "cpp", SourceFD,
                                                    SourceFileName));
    llvm::ToolOutputFile SourceFile(SourceFileName, SourceFD);
    SourceFile.os() << SourceText;
    SourceFile.os().flush();
    EXPECT_TRUE(llvm::sys::fs::exists(SourceFileName));

    std::unique_ptr<ASTUnit> ASTWithDefinition =
        tooling::buildASTFromCode(SourceText, SourceFileName);
    ASTWithDefinition->Save(ASTFileName.str());
    EXPECT_TRUE(llvm::sys::fs::exists(ASTFileName));

    // Load the definition from the AST file.
    llvm::Expected<const FunctionDecl *> NewFDorError = handleExpected(
        CTU.getCrossTUDefinition(FD, "", IndexFileName, false),
        []() { return nullptr; }, [](IndexError &) {});

    if (NewFDorError) {
      const FunctionDecl *NewFD = *NewFDorError;
      *Success = NewFD && NewFD->hasBody() && !OrigFDHasBody;

      if (NewFD) {
        // Check GetImportedFromSourceLocation.
        llvm::Optional<std::pair<SourceLocation, ASTUnit *>> SLocResult =
            CTU.getImportedFromSourceLocation(NewFD->getLocation());
        EXPECT_TRUE(SLocResult);
        if (SLocResult) {
          SourceLocation OrigSLoc = (*SLocResult).first;
          ASTUnit *OrigUnit = (*SLocResult).second;
          // OrigUnit is created internally by CTU (is not the
          // ASTWithDefinition).
          TranslationUnitDecl *OrigTU =
              OrigUnit->getASTContext().getTranslationUnitDecl();
          const FunctionDecl *FDWithDefinition = FindFInTU(OrigTU);
          EXPECT_TRUE(FDWithDefinition);
          if (FDWithDefinition) {
            EXPECT_EQ(FDWithDefinition->getName(), "f");
            EXPECT_TRUE(FDWithDefinition->isThisDeclarationADefinition());
            EXPECT_EQ(OrigSLoc, FDWithDefinition->getLocation());
          }
        }
      }
    }
  }

private:
  CrossTranslationUnitContext CTU;
  bool *Success;
};

class CTUAction : public clang::ASTFrontendAction {
public:
  CTUAction(bool *Success, unsigned OverrideLimit)
      : Success(Success), OverrideLimit(OverrideLimit) {}

protected:
  std::unique_ptr<clang::ASTConsumer>
  CreateASTConsumer(clang::CompilerInstance &CI, StringRef) override {
    CI.getAnalyzerOpts()->CTUImportThreshold = OverrideLimit;
    return llvm::make_unique<CTUASTConsumer>(CI, Success);
  }

private:
  bool *Success;
  const unsigned OverrideLimit;
};

} // end namespace

TEST(CrossTranslationUnit, CanLoadFunctionDefinition) {
  bool Success = false;
  EXPECT_TRUE(
      tooling::runToolOnCode(new CTUAction(&Success, 1u), "int f(int);"));
  EXPECT_TRUE(Success);
}

TEST(CrossTranslationUnit, RespectsLoadThreshold) {
  bool Success = false;
  EXPECT_TRUE(
      tooling::runToolOnCode(new CTUAction(&Success, 0u), "int f(int);"));
  EXPECT_FALSE(Success);
}

TEST(CrossTranslationUnit, IndexFormatCanBeParsed) {
  llvm::StringMap<std::string> Index;
  Index["a"] = "/b/f1";
  Index["c"] = "/d/f2";
  Index["e"] = "/f/f3";
  std::string IndexText = createCrossTUIndexString(Index);

  int IndexFD;
  llvm::SmallString<256> IndexFileName;
  ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("index", "txt", IndexFD,
                                                  IndexFileName));
  llvm::ToolOutputFile IndexFile(IndexFileName, IndexFD);
  IndexFile.os() << IndexText;
  IndexFile.os().flush();
  EXPECT_TRUE(llvm::sys::fs::exists(IndexFileName));
  llvm::Expected<llvm::StringMap<std::string>> IndexOrErr =
      parseCrossTUIndex(IndexFileName, "");
  EXPECT_TRUE((bool)IndexOrErr);
  llvm::StringMap<std::string> ParsedIndex = IndexOrErr.get();
  for (const auto &E : Index) {
    EXPECT_TRUE(ParsedIndex.count(E.getKey()));
    EXPECT_EQ(ParsedIndex[E.getKey()], E.getValue());
  }
  for (const auto &E : ParsedIndex)
    EXPECT_TRUE(Index.count(E.getKey()));
}

TEST(CrossTranslationUnit, CTUDirIsHandledCorrectly) {
  llvm::StringMap<std::string> Index;
  Index["a"] = "/b/c/d";
  std::string IndexText = createCrossTUIndexString(Index);

  int IndexFD;
  llvm::SmallString<256> IndexFileName;
  ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("index", "txt", IndexFD,
                                                  IndexFileName));
  llvm::ToolOutputFile IndexFile(IndexFileName, IndexFD);
  IndexFile.os() << IndexText;
  IndexFile.os().flush();
  EXPECT_TRUE(llvm::sys::fs::exists(IndexFileName));
  llvm::Expected<llvm::StringMap<std::string>> IndexOrErr =
      parseCrossTUIndex(IndexFileName, "/ctudir");
  EXPECT_TRUE((bool)IndexOrErr);
  llvm::StringMap<std::string> ParsedIndex = IndexOrErr.get();
  EXPECT_EQ(ParsedIndex["a"], "/ctudir/b/c/d");
}

} // end namespace cross_tu
} // end namespace clang
