llvm-project/clang/unittests/Basic/DiagnosticTest.cpp
kadir çetinkaya 41e3919ded
[clang] Introduce diagnostics suppression mappings (#112517)
This implements

https://discourse.llvm.org/t/rfc-add-support-for-controlling-diagnostics-severities-at-file-level-granularity-through-command-line/81292.

Users now can suppress warnings for certain headers by providing a
mapping with globs, a sample file looks like:
```
[unused]
src:*
src:*clang/*=emit
```

This will suppress warnings from `-Wunused` group in all files that
aren't under `clang/` directory. This mapping file can be passed to
clang via `--warning-suppression-mappings=foo.txt`.

At a high level, mapping file is stored in DiagnosticOptions and then
processed with rest of the warning flags when creating a
DiagnosticsEngine. This is a functor that uses SpecialCaseLists
underneath to match against globs coming from the mappings file.

This implies processing warning options now performs IO, relevant
interfaces are updated to take in a VFS, falling back to RealFileSystem
when one is not available.
2024-11-12 10:53:43 +01:00

340 lines
13 KiB
C++

//===- 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/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 <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;
}
private:
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());
EXPECT_TRUE(
Diags.isSuppressedViaMapping(diag::warn_unused_function, "foo.cpp"));
EXPECT_FALSE(Diags.isSuppressedViaMapping(diag::warn_deprecated, "foo.cpp"));
}
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, "bar.cpp"));
EXPECT_FALSE(
Diags.isSuppressedViaMapping(diag::warn_unused_function, "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,
"clang/lib/Basic/foo.h"));
EXPECT_FALSE(Diags.isSuppressedViaMapping(diag::warn_unused_function,
"clang/lib/Sema/bar.h"));
EXPECT_TRUE(Diags.isSuppressedViaMapping(diag::warn_unused_function,
"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());
FileManager FM({}, FS);
SourceManager SM(Diags, FM);
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