//===- unittests/Basic/DiagnosticTest.cpp -- Diagnostic engine 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/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticError.h"
#include "clang/Basic/DiagnosticIDs.h"
#include "clang/Basic/DiagnosticLex.h"
#include "clang/Basic/DiagnosticSema.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <memory>
#include <optional>
#include <vector>

using namespace llvm;
using namespace clang;

// Declare DiagnosticsTestHelper to avoid GCC warning
namespace clang {
void DiagnosticsTestHelper(DiagnosticsEngine &diag);
}

void clang::DiagnosticsTestHelper(DiagnosticsEngine &diag) {
  EXPECT_FALSE(diag.DiagStates.empty());
  EXPECT_TRUE(diag.DiagStatesByLoc.empty());
  EXPECT_TRUE(diag.DiagStateOnPushStack.empty());
}

namespace {
using testing::AllOf;
using testing::ElementsAre;
using testing::IsEmpty;

// Check that DiagnosticErrorTrap works with SuppressAllDiagnostics.
TEST(DiagnosticTest, suppressAndTrap) {
  DiagnosticsEngine Diags(new DiagnosticIDs(),
                          new DiagnosticOptions,
                          new IgnoringDiagConsumer());
  Diags.setSuppressAllDiagnostics(true);

  {
    DiagnosticErrorTrap trap(Diags);

    // Diag that would set UncompilableErrorOccurred and ErrorOccurred.
    Diags.Report(diag::err_target_unknown_triple) << "unknown";

    // Diag that would set UnrecoverableErrorOccurred and ErrorOccurred.
    Diags.Report(diag::err_cannot_open_file) << "file" << "error";

    // Diag that would set FatalErrorOccurred
    // (via non-note following a fatal error).
    Diags.Report(diag::warn_mt_message) << "warning";

    EXPECT_TRUE(trap.hasErrorOccurred());
    EXPECT_TRUE(trap.hasUnrecoverableErrorOccurred());
  }

  EXPECT_FALSE(Diags.hasErrorOccurred());
  EXPECT_FALSE(Diags.hasFatalErrorOccurred());
  EXPECT_FALSE(Diags.hasUncompilableErrorOccurred());
  EXPECT_FALSE(Diags.hasUnrecoverableErrorOccurred());
}

// Check that FatalsAsError works as intended
TEST(DiagnosticTest, fatalsAsError) {
  for (unsigned FatalsAsError = 0; FatalsAsError != 2; ++FatalsAsError) {
    DiagnosticsEngine Diags(new DiagnosticIDs(),
                            new DiagnosticOptions,
                            new IgnoringDiagConsumer());
    Diags.setFatalsAsError(FatalsAsError);

    // Diag that would set UnrecoverableErrorOccurred and ErrorOccurred.
    Diags.Report(diag::err_cannot_open_file) << "file" << "error";

    // Diag that would set FatalErrorOccurred
    // (via non-note following a fatal error).
    Diags.Report(diag::warn_mt_message) << "warning";

    EXPECT_TRUE(Diags.hasErrorOccurred());
    EXPECT_EQ(Diags.hasFatalErrorOccurred(), FatalsAsError ? 0u : 1u);
    EXPECT_TRUE(Diags.hasUncompilableErrorOccurred());
    EXPECT_TRUE(Diags.hasUnrecoverableErrorOccurred());

    // The warning should be emitted and counted only if we're not suppressing
    // after fatal errors.
    EXPECT_EQ(Diags.getNumWarnings(), FatalsAsError);
  }
}

TEST(DiagnosticTest, tooManyErrorsIsAlwaysFatal) {
  DiagnosticsEngine Diags(new DiagnosticIDs(), new DiagnosticOptions,
                          new IgnoringDiagConsumer());
  Diags.setFatalsAsError(true);

  // Report a fatal_too_many_errors diagnostic to ensure that still
  // acts as a fatal error despite downgrading fatal errors to errors.
  Diags.Report(diag::fatal_too_many_errors);
  EXPECT_TRUE(Diags.hasFatalErrorOccurred());

  // Ensure that the severity of that diagnostic is really "fatal".
  EXPECT_EQ(Diags.getDiagnosticLevel(diag::fatal_too_many_errors, {}),
            DiagnosticsEngine::Level::Fatal);
}

// Check that soft RESET works as intended
TEST(DiagnosticTest, softReset) {
  DiagnosticsEngine Diags(new DiagnosticIDs(), new DiagnosticOptions,
                          new IgnoringDiagConsumer());

  unsigned numWarnings = 0U, numErrors = 0U;

  Diags.Reset(true);
  // Check For ErrorOccurred and TrapNumErrorsOccurred
  EXPECT_FALSE(Diags.hasErrorOccurred());
  EXPECT_FALSE(Diags.hasFatalErrorOccurred());
  EXPECT_FALSE(Diags.hasUncompilableErrorOccurred());
  // Check for UnrecoverableErrorOccurred and TrapNumUnrecoverableErrorsOccurred
  EXPECT_FALSE(Diags.hasUnrecoverableErrorOccurred());

  EXPECT_EQ(Diags.getNumWarnings(), numWarnings);
  EXPECT_EQ(Diags.getNumErrors(), numErrors);

  // Check for private variables of DiagnosticsEngine differentiating soft reset
  DiagnosticsTestHelper(Diags);

  EXPECT_TRUE(Diags.isLastDiagnosticIgnored());
}

TEST(DiagnosticTest, diagnosticError) {
  DiagnosticsEngine Diags(new DiagnosticIDs(), new DiagnosticOptions,
                          new IgnoringDiagConsumer());
  PartialDiagnostic::DiagStorageAllocator Alloc;
  llvm::Expected<std::pair<int, int>> Value = DiagnosticError::create(
      SourceLocation(), PartialDiagnostic(diag::err_cannot_open_file, Alloc)
                            << "file"
                            << "error");
  ASSERT_TRUE(!Value);
  llvm::Error Err = Value.takeError();
  std::optional<PartialDiagnosticAt> ErrDiag = DiagnosticError::take(Err);
  llvm::cantFail(std::move(Err));
  ASSERT_FALSE(!ErrDiag);
  EXPECT_EQ(ErrDiag->first, SourceLocation());
  EXPECT_EQ(ErrDiag->second.getDiagID(), diag::err_cannot_open_file);

  Value = std::make_pair(20, 1);
  ASSERT_FALSE(!Value);
  EXPECT_EQ(*Value, std::make_pair(20, 1));
  EXPECT_EQ(Value->first, 20);
}

TEST(DiagnosticTest, storedDiagEmptyWarning) {
  DiagnosticsEngine Diags(new DiagnosticIDs(), new DiagnosticOptions);

  class CaptureDiagnosticConsumer : public DiagnosticConsumer {
  public:
    SmallVector<StoredDiagnostic> StoredDiags;

    void HandleDiagnostic(DiagnosticsEngine::Level level,
                          const Diagnostic &Info) override {
      StoredDiags.push_back(StoredDiagnostic(level, Info));
    }
  };

  CaptureDiagnosticConsumer CaptureConsumer;
  Diags.setClient(&CaptureConsumer, /*ShouldOwnClient=*/false);
  Diags.Report(diag::pp_hash_warning) << "";
  ASSERT_TRUE(CaptureConsumer.StoredDiags.size() == 1);

  // Make sure an empty warning can round-trip with \c StoredDiagnostic.
  Diags.Report(CaptureConsumer.StoredDiags.front());
}

class SuppressionMappingTest : public testing::Test {
public:
  SuppressionMappingTest() {
    Diags.setClient(&CaptureConsumer, /*ShouldOwnClient=*/false);
  }

protected:
  llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> FS =
      llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
  DiagnosticsEngine Diags{new DiagnosticIDs(), new DiagnosticOptions};

  llvm::ArrayRef<StoredDiagnostic> diags() {
    return CaptureConsumer.StoredDiags;
  }

  SourceLocation locForFile(llvm::StringRef FileName) {
    auto Buf = MemoryBuffer::getMemBuffer("", FileName);
    SourceManager &SM = Diags.getSourceManager();
    FileID FooID = SM.createFileID(std::move(Buf));
    return SM.getLocForStartOfFile(FooID);
  }

private:
  FileManager FM{{}, FS};
  SourceManager SM{Diags, FM};

  class CaptureDiagnosticConsumer : public DiagnosticConsumer {
  public:
    std::vector<StoredDiagnostic> StoredDiags;

    void HandleDiagnostic(DiagnosticsEngine::Level level,
                          const Diagnostic &Info) override {
      StoredDiags.push_back(StoredDiagnostic(level, Info));
    }
  };
  CaptureDiagnosticConsumer CaptureConsumer;
};

MATCHER_P(WithMessage, Msg, "has diagnostic message") {
  return arg.getMessage() == Msg;
}
MATCHER(IsError, "has error severity") {
  return arg.getLevel() == DiagnosticsEngine::Level::Error;
}

TEST_F(SuppressionMappingTest, MissingMappingFile) {
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(), ElementsAre(AllOf(
                           WithMessage("no such file or directory: 'foo.txt'"),
                           IsError())));
}

TEST_F(SuppressionMappingTest, MalformedFile) {
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer("asdf", "foo.txt"));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(),
              ElementsAre(AllOf(
                  WithMessage("failed to process suppression mapping file "
                              "'foo.txt': malformed line 1: 'asdf'"),
                  IsError())));
}

TEST_F(SuppressionMappingTest, UnknownDiagName) {
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer("[non-existing-warning]"));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(), ElementsAre(WithMessage(
                           "unknown warning option 'non-existing-warning'")));
}

TEST_F(SuppressionMappingTest, SuppressesGroup) {
  llvm::StringLiteral SuppressionMappingFile = R"(
  [unused]
  src:*)";
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer(SuppressionMappingFile));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(), IsEmpty());

  SourceLocation FooLoc = locForFile("foo.cpp");
  EXPECT_TRUE(Diags.isSuppressedViaMapping(diag::warn_unused_function, FooLoc));
  EXPECT_FALSE(Diags.isSuppressedViaMapping(diag::warn_deprecated, FooLoc));
}

TEST_F(SuppressionMappingTest, EmitCategoryIsExcluded) {
  llvm::StringLiteral SuppressionMappingFile = R"(
  [unused]
  src:*
  src:*foo.cpp=emit)";
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer(SuppressionMappingFile));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(), IsEmpty());

  EXPECT_TRUE(Diags.isSuppressedViaMapping(diag::warn_unused_function,
                                           locForFile("bar.cpp")));
  EXPECT_FALSE(Diags.isSuppressedViaMapping(diag::warn_unused_function,
                                            locForFile("foo.cpp")));
}

TEST_F(SuppressionMappingTest, LongestMatchWins) {
  llvm::StringLiteral SuppressionMappingFile = R"(
  [unused]
  src:*clang/*
  src:*clang/lib/Sema/*=emit
  src:*clang/lib/Sema/foo*)";
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer(SuppressionMappingFile));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  EXPECT_THAT(diags(), IsEmpty());

  EXPECT_TRUE(Diags.isSuppressedViaMapping(
      diag::warn_unused_function, locForFile("clang/lib/Basic/foo.h")));
  EXPECT_FALSE(Diags.isSuppressedViaMapping(
      diag::warn_unused_function, locForFile("clang/lib/Sema/bar.h")));
  EXPECT_TRUE(Diags.isSuppressedViaMapping(diag::warn_unused_function,
                                           locForFile("clang/lib/Sema/foo.h")));
}

TEST_F(SuppressionMappingTest, IsIgnored) {
  llvm::StringLiteral SuppressionMappingFile = R"(
  [unused]
  src:*clang/*)";
  Diags.getDiagnosticOptions().DiagnosticSuppressionMappingsFile = "foo.txt";
  Diags.getDiagnosticOptions().Warnings = {"unused"};
  FS->addFile("foo.txt", /*ModificationTime=*/{},
              llvm::MemoryBuffer::getMemBuffer(SuppressionMappingFile));
  clang::ProcessWarningOptions(Diags, Diags.getDiagnosticOptions(), *FS);
  ASSERT_THAT(diags(), IsEmpty());

  SourceManager &SM = Diags.getSourceManager();
  auto ClangID =
      SM.createFileID(llvm::MemoryBuffer::getMemBuffer("", "clang/foo.h"));
  auto NonClangID =
      SM.createFileID(llvm::MemoryBuffer::getMemBuffer("", "llvm/foo.h"));
  auto PresumedClangID =
      SM.createFileID(llvm::MemoryBuffer::getMemBuffer("", "llvm/foo2.h"));
  // Add a line directive to point into clang/foo.h
  SM.AddLineNote(SM.getLocForStartOfFile(PresumedClangID), 42,
                 SM.getLineTableFilenameID("clang/foo.h"), false, false,
                 clang::SrcMgr::C_User);

  EXPECT_TRUE(Diags.isIgnored(diag::warn_unused_function,
                              SM.getLocForStartOfFile(ClangID)));
  EXPECT_FALSE(Diags.isIgnored(diag::warn_unused_function,
                               SM.getLocForStartOfFile(NonClangID)));
  EXPECT_TRUE(Diags.isIgnored(diag::warn_unused_function,
                              SM.getLocForStartOfFile(PresumedClangID)));

  // Pretend we have a clang-diagnostic pragma to enforce the warning. Make sure
  // suppressing mapping doesn't take over.
  Diags.setSeverity(diag::warn_unused_function, diag::Severity::Error,
                    SM.getLocForStartOfFile(ClangID));
  EXPECT_FALSE(Diags.isIgnored(diag::warn_unused_function,
                               SM.getLocForStartOfFile(ClangID)));
}
} // namespace
