//===- 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 #include #include 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> Value = DiagnosticError::create( SourceLocation(), PartialDiagnostic(diag::err_cannot_open_file, Alloc) << "file" << "error"); ASSERT_TRUE(!Value); llvm::Error Err = Value.takeError(); std::optional 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 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 FS = llvm::makeIntrusiveRefCnt(); DiagnosticsEngine Diags{new DiagnosticIDs(), new DiagnosticOptions}; llvm::ArrayRef 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 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