Index: clang/include/clang/Analysis/PathDiagnosticConverterDiagnosticConsumer.h =================================================================== --- /dev/null +++ clang/include/clang/Analysis/PathDiagnosticConverterDiagnosticConsumer.h @@ -0,0 +1,90 @@ +//===--- PathDiagnosticConverterDiagnosticConsumer.h ------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +/// +/// \file +/// This file defines a Clang DiagnosticConsumer that converts Clang +/// diagnostics into PathDiagnostics. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_ANALYSIS_PATHDIAGNOSTICCONVERTERDIAGNOSTICCONSUMER_H +#define LLVM_CLANG_ANALYSIS_PATHDIAGNOSTICCONVERTERDIAGNOSTICCONSUMER_H + +#include "clang/Analysis/PathDiagnostic.h" +#include "clang/Analysis/PathDiagnosticConsumers.h" + +namespace clang { +namespace ento { + +/// \brief A Clang diagnostic consumer that converts Clang diagnostics into +/// "path diagnostics" commonly used by the Static Analyzer. +/// +/// PathDiagnostics are immediately pushed into PathDiagnosticConsumers +/// that can display clang diagnostics in a number of different ways, +/// either simply for the visuals or possibly for achieving consistency +/// with other tools that already use path diagnostics. +/// If an existing tool already uses a PathDiagnosticConsumer that converts +/// path diagnostics into clang diagnostics (eg., TextPathDiagnosticConsumer), +/// putting such diagnostics back into PathDiagnosticConverterDiagnosticConsumer +/// is undesirable because the original conversion to Clang diagnostics +/// is not lossless. Instead such tool is supposed to keep accessing +/// PathDiagnosticConsumers directly. PathDiagnosticConsumers are expected +/// to be constructed and managed separately. +class PathDiagnosticConverterDiagnosticConsumer : public DiagnosticConsumer { +public: + struct BugTypeInfo { + StringRef CheckName; + StringRef BugType; + StringRef BugCategory; + }; + + using BugTypeInfoProvider = std::function; + +private: + PathDiagnosticConsumers &PathConsumers; + BugTypeInfoProvider BTIProvider; + bool ShouldDisplayNotesAsEvents; + bool ShouldTrimCheckerName; + + llvm::DenseMap> + PartialPDs; + + void flushPartialDiagnostic(); + +public: + /// Construct a PathDiagnosticConverterDiagnosticConsumer with existing + /// PathDiagnosticConsumers and the necessary configuration. + /// \param PathConsumers The list of PathDiagnosticConsumers to work with. + /// \param BTIProvider A callback that provides additional metadata + /// about each diagnostic that's necessary for path diagnostics + /// but typically not present in regular Clang diagnostics. + /// \param ShouldDisplayNotesAsEvents Whether display note diagnostics as path + // events or non-path notes. The latter is always the intended + // behavior as long as the client actually understands non-path notes. + /// \param ShouldTrimCheckerName Whether incoming diagnostics are suffixed + // with [checker name] which we can automatically trim. + PathDiagnosticConverterDiagnosticConsumer( + PathDiagnosticConsumers &PathConsumers, BugTypeInfoProvider &&BTIProvider, + bool ShouldDisplayNotesAsEvents, bool ShouldTrimCheckerName) + : PathConsumers(PathConsumers), BTIProvider(std::move(BTIProvider)), + ShouldDisplayNotesAsEvents(ShouldDisplayNotesAsEvents), + ShouldTrimCheckerName(ShouldTrimCheckerName) {} + + void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, + const Diagnostic &Info) override; + + /// Inform the consumer that the last diagnostic has been sent. This is + /// necessary because the consumer does not know whether more notes are + /// going to be attached to the last warning. + void finish() override { flushPartialDiagnostic(); } +}; + +} // end 'ento' namespace +} // end 'clang' namespace + +#endif Index: clang/lib/Analysis/CMakeLists.txt =================================================================== --- clang/lib/Analysis/CMakeLists.txt +++ clang/lib/Analysis/CMakeLists.txt @@ -23,6 +23,7 @@ LiveVariables.cpp ObjCNoReturn.cpp PathDiagnostic.cpp + PathDiagnosticConverterDiagnosticConsumer.cpp PlistPathDiagnosticConsumer.cpp PlistHTMLPathDiagnosticConsumer.cpp PostOrderCFGView.cpp Index: clang/lib/Analysis/PathDiagnosticConverterDiagnosticConsumer.cpp =================================================================== --- /dev/null +++ clang/lib/Analysis/PathDiagnosticConverterDiagnosticConsumer.cpp @@ -0,0 +1,112 @@ +//===--- PathDiagnosticConverterDiagnosticConsumer.cpp ----------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// This file defines the PathDiagnosticConverterDiagnosticConsumer object +// which converts regular Clang diagnostics into PathDiagnostics. +// +//===----------------------------------------------------------------------===// + +#include "clang/Analysis/PathDiagnosticConverterDiagnosticConsumer.h" + +using namespace clang; +using namespace ento; + +void PathDiagnosticConverterDiagnosticConsumer::flushPartialDiagnostic() { + if (PartialPDs.empty()) + return; + + assert(PartialPDs.size() == PathConsumers.size() && + "Path diagnostics were constructed for not all consumers"); + + for (PathDiagnosticConsumer *Consumer : PathConsumers) + Consumer->HandlePathDiagnostic(std::move(PartialPDs[Consumer])); + + PartialPDs.clear(); +} + +static void convertDiagnosticData(const Diagnostic &Info, + PathDiagnosticPiece &Piece) { + for (const auto &R : Info.getRanges()) + Piece.addRange(R.getAsRange()); + + // FIXME: Convert more data, eg. fix-it hints. +} + +void PathDiagnosticConverterDiagnosticConsumer::HandleDiagnostic( + DiagnosticsEngine::Level DiagLevel, const Diagnostic &Info) { + // Count warnings/errors. + DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info); + + llvm::SmallString<128> Msg; + Info.FormatDiagnostic(Msg); + + // Auto-capitalize. Path diagnostics are traditionally treated as + // full sentences whereas Clang diagnostics are traditionally lowercase. + assert(Msg.size() > 0 && "Empty diagnostic message"); + Msg[0] = std::toupper(Msg[0]); + + // Optionally try to remove checker name. + StringRef CleanMsg; // PathDiagnosticPiece will copy it. + if (ShouldTrimCheckerName) { + size_t I = Msg.str().rfind('['); + assert(I != std::string::npos && "Checker name not detected in diagnostic"); + assert(I > 0 && "Diagnostic message contains only checker name"); + CleanMsg = Msg.str().substr(0, I - 1); + } else { + CleanMsg = Msg; + } + + SourceLocation Loc = Info.getLocation(); + const SourceManager &SM = Info.getSourceManager(); + + PathDiagnosticLocation PDLoc(Loc, SM); + + // For now we assume that every diagnostic is either a warning or a note + // logically attached to a previous warning. + // Notes do not come in actually attached to their respective warnings + // but are fed to us independently. The good news is they always follow their + // respective warnings and come in in the right order so we can + // automatically re-attach them. + if (DiagLevel == DiagnosticsEngine::Note) { + for (PathDiagnosticConsumer *Consumer : PathConsumers) { + PathDiagnosticPieceRef Piece; + if (ShouldDisplayNotesAsEvents) { + Piece = std::make_shared(PDLoc, CleanMsg); + } else { + Piece = std::make_shared(PDLoc, CleanMsg); + } + + convertDiagnosticData(Info, *Piece); + + PartialPDs[Consumer]->getMutablePieces().push_back(Piece); + } + } else { + assert((DiagLevel == DiagnosticsEngine::Warning || + DiagLevel == DiagnosticsEngine::Error) && + "Unsupported diagnostic level"); + + // No more incoming notes. The current path diagnostic is complete. + flushPartialDiagnostic(); + + BugTypeInfo BTI = BTIProvider(Info); + + for (PathDiagnosticConsumer *Consumer : PathConsumers) { + auto PD = std::make_unique( + BTI.CheckName, /*DeclWithIssue=*/nullptr, BTI.BugType, + CleanMsg, CleanMsg, BTI.BugCategory, PDLoc, /*DeclToUnique=*/nullptr, + std::make_unique()); + + PathDiagnosticPieceRef Piece = + std::make_shared(PDLoc, CleanMsg); + convertDiagnosticData(Info, *Piece); + PD->setEndOfPath(std::move(Piece)); + + PartialPDs[Consumer] = std::move(PD); + } + } +} Index: clang/unittests/Analysis/CMakeLists.txt =================================================================== --- clang/unittests/Analysis/CMakeLists.txt +++ clang/unittests/Analysis/CMakeLists.txt @@ -8,6 +8,7 @@ CFGTest.cpp CloneDetectionTest.cpp ExprMutationAnalyzerTest.cpp + PathDiagnosticConverterDiagnosticConsumer.cpp ) clang_target_link_libraries(ClangAnalysisTests Index: clang/unittests/Analysis/PathDiagnosticConverterDiagnosticConsumer.cpp =================================================================== --- /dev/null +++ clang/unittests/Analysis/PathDiagnosticConverterDiagnosticConsumer.cpp @@ -0,0 +1,246 @@ +//===- PathDiagnosticConverterDiagnosticConsumer.cpp ------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +/// +/// \file +/// This file provides unittests for PathDiagnosticConverterDiagnosticConsumer. +/// +//===----------------------------------------------------------------------===// + +#include "clang/Analysis/PathDiagnosticConverterDiagnosticConsumer.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Tooling/Tooling.h" +#include "gtest/gtest.h" + +using namespace clang; +using namespace ento; +using namespace llvm; + +struct ExpectedLocationTy { + unsigned Line; + unsigned Column; + + void testEquality(SourceLocation L, SourceManager &SM) const { + EXPECT_EQ(SM.getSpellingLineNumber(L), Line); + EXPECT_EQ(SM.getSpellingColumnNumber(L), Column); + } +}; + +struct ExpectedRangeTy { + ExpectedLocationTy Begin; + ExpectedLocationTy End; + + void testEquality(SourceRange R, SourceManager &SM) const { + Begin.testEquality(R.getBegin(), SM); + End.testEquality(R.getEnd(), SM); + } +}; + +struct ExpectedPieceTy { + ExpectedLocationTy Loc; + std::string Text; + std::vector Ranges; + + void testEquality(const PathDiagnosticPiece &Piece, SourceManager &SM) { + Loc.testEquality(Piece.getLocation().asLocation(), SM); + EXPECT_EQ(Piece.getString(), Text); + EXPECT_EQ(Ranges.size(), Piece.getRanges().size()); + for (const auto &RangeItem : llvm::enumerate(Piece.getRanges())) + Ranges[RangeItem.index()].testEquality(RangeItem.value(), SM); + } +}; + +struct ExpectedDiagTy { + ExpectedLocationTy Loc; + std::string VerboseDescription; + std::string ShortDescription; + std::string CheckerName; + std::string BugType; + std::string Category; + std::vector Path; + + void testEquality(const PathDiagnostic &Diag, SourceManager &SM) { + Loc.testEquality(Diag.getLocation().asLocation(), SM); + EXPECT_EQ(Diag.getVerboseDescription(), VerboseDescription); + EXPECT_EQ(Diag.getShortDescription(), ShortDescription); + EXPECT_EQ(Diag.getCheckerName(), CheckerName); + EXPECT_EQ(Diag.getBugType(), BugType); + EXPECT_EQ(Diag.getCategory(), Category); + + EXPECT_EQ(Path.size(), Diag.path.size()); + for (const auto &PieceItem : llvm::enumerate(Diag.path)) + Path[PieceItem.index()].testEquality(*PieceItem.value(), SM); + } +}; + +using ExpectedDiagsTy = std::vector; + +namespace { +class TestPathDiagnosticConsumer : public PathDiagnosticConsumer { + ExpectedDiagsTy ExpectedDiags; + SourceManager &SM; + +public: + TestPathDiagnosticConsumer(ExpectedDiagsTy &&ExpectedDiags, SourceManager &SM) + : ExpectedDiags(std::move(ExpectedDiags)), SM(SM) {} + + StringRef getName() const override { return "test"; } + + void FlushDiagnosticsImpl(std::vector &Diags, + FilesMade *filesMade) override { + EXPECT_EQ(Diags.size(), ExpectedDiags.size()); + for (const auto &Item : llvm::enumerate(Diags)) + ExpectedDiags[Item.index()].testEquality(*Item.value(), SM); + } +}; + +PathDiagnosticConverterDiagnosticConsumer::BugTypeInfo +bugTypeInfoProvider(const Diagnostic &) { + return PathDiagnosticConverterDiagnosticConsumer::BugTypeInfo{ + "test check name", "test bug type", "test bug category"}; +} + +class TestASTConsumer : public ASTConsumer { + ExpectedDiagsTy ExpectedDiags; + ASTContext &ACtx; + + void performTest(const Decl *D) { + if (!D->hasBody()) + return; + + assert(!ExpectedDiags.empty() && "Consumer invoked more than once!"); + TestPathDiagnosticConsumer TestPDC{std::move(ExpectedDiags), + ACtx.getSourceManager()}; + // Sanitize storage after move so that the above assertion actually worked. + ExpectedDiags.clear(); + + PathDiagnosticConsumers PathConsumers = {&TestPDC}; + + llvm::IntrusiveRefCntPtr DiagEngine = + new DiagnosticsEngine(new DiagnosticIDs, new DiagnosticOptions); + PathDiagnosticConverterDiagnosticConsumer DiagConsumer( + PathConsumers, bugTypeInfoProvider, + /*ShouldDisplayNotesAsEvents=*/true, /*ShouldTrimCheckerName=*/false); + DiagEngine->setClient(&DiagConsumer, /*ShouldOwnClient=*/false); + DiagEngine->setSourceManager(&ACtx.getSourceManager()); + + unsigned WarningDiagID = DiagEngine->getCustomDiagID( + DiagnosticsEngine::Warning, "bar is called"); + unsigned NoteDiagID = DiagEngine->getCustomDiagID( + DiagnosticsEngine::Note, "with argument at least %0"); + + const CompoundStmt *Body = cast(D->getBody()); + for (const Stmt *S : Body->children()) { + const auto *CE = cast(S); + DiagEngine->Report(CE->getBeginLoc(), WarningDiagID) + << CE->getSourceRange(); + + const auto *Arg = cast(CE->getArg(0)); + const llvm::APSInt &IArg = *Arg->getIntegerConstantExpr(ACtx); + for (uint64_t I = 0; I < IArg.getZExtValue(); ++I) + DiagEngine->Report(Arg->getBeginLoc(), NoteDiagID) + << (int)(I + 1) << Arg->getSourceRange(); + } + + // The last diagnostic must be flushed manually! + DiagConsumer.finish(); + + PathDiagnosticConsumer::FilesMade FM; + TestPDC.FlushDiagnostics(&FM); + } + +public: + TestASTConsumer(CompilerInstance &CI, ExpectedDiagsTy &&ExpectedDiags) + : ExpectedDiags(std::move(ExpectedDiags)), ACtx(CI.getASTContext()) {} + + bool HandleTopLevelDecl(DeclGroupRef DG) override { + for (auto *D : DG) + performTest(D); + + return true; + } +}; + +class TestAction : public ASTFrontendAction { + ExpectedDiagsTy ExpectedDiags; + +public: + TestAction(ExpectedDiagsTy &&ExpectedDiags) + : ExpectedDiags(std::move(ExpectedDiags)) {} + + std::unique_ptr CreateASTConsumer(CompilerInstance &CI, + StringRef File) override { + return std::make_unique(CI, std::move(ExpectedDiags)); + } +}; + +TEST(PathDiagnosticConverter, ConsumeWarningsAndNotes) { + // The consumer would emit a warning against every call of 'bar()' + // and attach as many notes to it as the argument of 'bar()' says. + // Expected results are listed and should match actual results + // for the test to pass. + EXPECT_TRUE(tooling::runToolOnCode( + std::make_unique(ExpectedDiagsTy{ + {{3, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{3, 3}, "Bar is called", {{{3, 3}, {3, 8}}}}}}, + {{4, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{4, 3}, "Bar is called", {{{4, 3}, {4, 8}}}}}}, + {{5, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{5, 3}, "Bar is called", {{{5, 3}, {5, 8}}}}}}}), + "void bar(int);\nvoid foo() {\n bar(0);\n bar(0);\n bar(0);\n}\n", + "input.c")); + + EXPECT_TRUE(tooling::runToolOnCode( + std::make_unique(ExpectedDiagsTy{ + {{3, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{3, 3}, "Bar is called", {{{3, 3}, {3, 8}}}}, + {{3, 7}, "With argument at least 1", {{{3, 7}, {3, 7}}}}, + {{3, 7}, "With argument at least 2", {{{3, 7}, {3, 7}}}}}}}), + "void bar(int);\nvoid foo() {\n bar(2);\n}\n", "input.c")); + + EXPECT_TRUE(tooling::runToolOnCode( + std::make_unique(ExpectedDiagsTy{ + {{3, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{3, 3}, "Bar is called", {{{3, 3}, {3, 8}}}}, + {{3, 7}, "With argument at least 1", {{{3, 7}, {3, 7}}}}, + {{3, 7}, "With argument at least 2", {{{3, 7}, {3, 7}}}}}}, + {{4, 3}, + "Bar is called", + "Bar is called", + "test check name", + "test bug type", + "test bug category", + {{{4, 3}, "Bar is called", {{{4, 3}, {4, 8}}}}, + {{4, 7}, "With argument at least 1", {{{4, 7}, {4, 7}}}}}}}), + "void bar(int);\nvoid foo() {\n bar(2);\n bar(1);\n}\n", "input.c")); +} +} // namespace