diff --git a/clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp b/clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp --- a/clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp +++ b/clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp @@ -52,6 +52,7 @@ #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/Statistic.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" @@ -2812,12 +2813,19 @@ // Implementation of FalsePositiveRefutationBRVisitor. //===----------------------------------------------------------------------===// +#define DEBUG_TYPE "FalsePositiveRefutationBRVisitor" +STATISTIC(CrosscheckedBugReports, + "The # of bug reports which were checked for infeasible constraints"); +STATISTIC(CrosscheckInvalidatedBugReports, + "The # of bug reports invalidated due to infeasible constraints"); + FalsePositiveRefutationBRVisitor::FalsePositiveRefutationBRVisitor() : Constraints(ConstraintRangeTy::Factory().getEmptyMap()) {} void FalsePositiveRefutationBRVisitor::finalizeVisitor( BugReporterContext &BRC, const ExplodedNode *EndPathNode, PathSensitiveBugReport &BR) { + ++CrosscheckedBugReports; // Collect new constraints VisitNode(EndPathNode, BRC, BR); @@ -2848,8 +2856,10 @@ if (!isSat.hasValue()) return; - if (!isSat.getValue()) + if (!isSat.getValue()) { + ++CrosscheckInvalidatedBugReports; BR.markInvalid("Infeasible constraints", EndPathNode->getLocationContext()); + } } PathDiagnosticPieceRef FalsePositiveRefutationBRVisitor::VisitNode( diff --git a/clang/unittests/StaticAnalyzer/CMakeLists.txt b/clang/unittests/StaticAnalyzer/CMakeLists.txt --- a/clang/unittests/StaticAnalyzer/CMakeLists.txt +++ b/clang/unittests/StaticAnalyzer/CMakeLists.txt @@ -7,6 +7,7 @@ AnalyzerOptionsTest.cpp CallDescriptionTest.cpp CallEventTest.cpp + FalsePositiveRefutationBRVisitorTest.cpp ParamRegionTest.cpp RangeSetTest.cpp RegisterCustomCheckersTest.cpp diff --git a/clang/unittests/StaticAnalyzer/CheckerRegistration.h b/clang/unittests/StaticAnalyzer/CheckerRegistration.h --- a/clang/unittests/StaticAnalyzer/CheckerRegistration.h +++ b/clang/unittests/StaticAnalyzer/CheckerRegistration.h @@ -14,6 +14,7 @@ #include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h" #include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h" #include "clang/Tooling/Tooling.h" +#include "gtest/gtest.h" namespace clang { namespace ento { @@ -65,10 +66,21 @@ } }; +inline SmallString<80> getCurrentTestNameAsFileName() { + const ::testing::TestInfo *Info = + ::testing::UnitTest::GetInstance()->current_test_info(); + + SmallString<80> FileName; + (Twine{Info->name()} + ".cc").toVector(FileName); + return FileName; +} + template bool runCheckerOnCode(const std::string &Code, std::string &Diags) { + const SmallVectorImpl &FileName = getCurrentTestNameAsFileName(); llvm::raw_string_ostream OS(Diags); - return tooling::runToolOnCode(std::make_unique>(OS), Code); + return tooling::runToolOnCode(std::make_unique>(OS), Code, + FileName); } template @@ -77,5 +89,22 @@ return runCheckerOnCode(Code, Diags); } +template +bool runCheckerOnCodeWithArgs(const std::string &Code, + const std::vector &Args, + std::string &Diags) { + const SmallVectorImpl &FileName = getCurrentTestNameAsFileName(); + llvm::raw_string_ostream OS(Diags); + return tooling::runToolOnCodeWithArgs( + std::make_unique>(OS), Code, Args, FileName); +} + +template +bool runCheckerOnCodeWithArgs(const std::string &Code, + const std::vector &Args) { + std::string Diags; + return runCheckerOnCodeWithArgs(Code, Args, Diags); +} + } // namespace ento } // namespace clang diff --git a/clang/unittests/StaticAnalyzer/FalsePositiveRefutationBRVisitorTest.cpp b/clang/unittests/StaticAnalyzer/FalsePositiveRefutationBRVisitorTest.cpp new file mode 100644 --- /dev/null +++ b/clang/unittests/StaticAnalyzer/FalsePositiveRefutationBRVisitorTest.cpp @@ -0,0 +1,175 @@ +//===- unittests/StaticAnalyzer/FalsePositiveRefutationBRVisitorTest.cpp --===// +// +// 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 "CheckerRegistration.h" +#include "Reusables.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" +#include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h" +#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h" +#include "llvm/Config/config.h" +#include "gtest/gtest.h" + +// FIXME: Use GTEST_SKIP() instead if GTest is updated to version 1.10.0 +#define SKIP_WITHOUT_Z3 \ + do \ + if (!LLVM_WITH_Z3) \ + return; \ + while (0) + +namespace clang { +namespace ento { +namespace { + +class FalsePositiveGenerator : public Checker { + using Self = FalsePositiveGenerator; + const BuiltinBug FalsePositiveGeneratorBug{this, "FalsePositiveGenerator"}; + using HandlerFn = bool (Self::*)(const CallEvent &Call, + CheckerContext &) const; + CallDescriptionMap Callbacks = { + {{"reachedWithContradiction", 0}, &Self::reachedWithContradiction}, + {{"reachedWithNoContradiction", 0}, &Self::reachedWithNoContradiction}, + {{"reportIfCanBeTrue", 1}, &Self::reportIfCanBeTrue}, + }; + + bool report(CheckerContext &C, ProgramStateRef State, + StringRef Description) const { + ExplodedNode *Node = C.generateNonFatalErrorNode(State); + if (!Node) + return false; + + auto Report = std::make_unique( + FalsePositiveGeneratorBug, Description, Node); + C.emitReport(std::move(Report)); + return true; + } + + bool reachedWithNoContradiction(const CallEvent &, CheckerContext &C) const { + return report(C, C.getState(), "REACHED_WITH_NO_CONTRADICTION"); + } + + bool reachedWithContradiction(const CallEvent &, CheckerContext &C) const { + return report(C, C.getState(), "REACHED_WITH_CONTRADICTION"); + } + + // Similar to ExprInspectionChecker::analyzerEval except it emits warning only + // if the argument can be true. The report emits the report in the state where + // the assertion true. + bool reportIfCanBeTrue(const CallEvent &Call, CheckerContext &C) const { + // A specific instantiation of an inlined function may have more constrained + // values than can generally be assumed. Skip the check. + if (C.getPredecessor()->getLocationContext()->getStackFrame()->getParent()) + return false; + + SVal AssertionVal = Call.getArgSVal(0); + if (AssertionVal.isUndef()) + return false; + + ProgramStateRef State = C.getPredecessor()->getState(); + ProgramStateRef StTrue; + std::tie(StTrue, std::ignore) = + State->assume(AssertionVal.castAs()); + if (StTrue) + return report(C, StTrue, "CAN_BE_TRUE"); + return false; + } + +public: + bool evalCall(const CallEvent &Call, CheckerContext &C) const { + if (const HandlerFn *Callback = Callbacks.lookup(Call)) + return (this->*(*Callback))(Call, C); + return false; + } +}; + +void addFalsePositiveGenerator(AnalysisASTConsumer &AnalysisConsumer, + AnalyzerOptions &AnOpts) { + AnOpts.CheckersAndPackages = {{"test.FalsePositiveGenerator", true}, + {"debug.ViewExplodedGraph", false}}; + AnalysisConsumer.AddCheckerRegistrationFn([](CheckerRegistry &Registry) { + Registry.addChecker( + "test.FalsePositiveGenerator", "EmptyDescription", "EmptyDocsUri"); + }); +} + +// C++20 use constexpr below. +const std::vector LazyAssumeArgs{ + "-Xclang", "-analyzer-config", "-Xclang", "eagerly-assume=false"}; +const std::vector LazyAssumeAndCrossCheckArgs{ + "-Xclang", "-analyzer-config", "-Xclang", "eagerly-assume=false", + "-Xclang", "-analyzer-config", "-Xclang", "crosscheck-with-z3=true"}; + +TEST(FalsePositiveRefutationBRVisitor, UnSatInTheMiddleNoReport) { + SKIP_WITHOUT_Z3; + constexpr auto Code = R"( + void reachedWithContradiction(); + void reachedWithNoContradiction(); + void test(int x, int y) { + if (x * y == 0) + return; + reachedWithNoContradiction(); + if (x == 0) { + reachedWithContradiction(); + // x * y != 0 => x != 0 && y != 0 => contradict with x == 0 + } + })"; + + std::string Diags; + EXPECT_TRUE(runCheckerOnCodeWithArgs( + Code, LazyAssumeAndCrossCheckArgs, Diags)); + EXPECT_EQ(Diags, + "test.FalsePositiveGenerator:REACHED_WITH_NO_CONTRADICTION\n"); + // Single warning. The second report was invalidated by the visitor. + + // Without enabling the crosscheck-with-z3 both reports are displayed. + std::string Diags2; + EXPECT_TRUE(runCheckerOnCodeWithArgs( + Code, LazyAssumeArgs, Diags2)); + EXPECT_EQ(Diags2, + "test.FalsePositiveGenerator:REACHED_WITH_NO_CONTRADICTION\n" + "test.FalsePositiveGenerator:REACHED_WITH_CONTRADICTION\n"); +} + +TEST(FalsePositiveRefutationBRVisitor, UnSatAtErrorNodeWithNewSymbolNoReport) { + SKIP_WITHOUT_Z3; + constexpr auto Code = R"( + void reportIfCanBeTrue(bool); + void reachedWithNoContradiction(); + void test(int x, int y) { + if (x * y == 0) + return; + // We know that 'x * y': {[MIN,-1], [1,MAX]} + reachedWithNoContradiction(); + reportIfCanBeTrue(x == 0); // contradiction + // The function introduces the 'x == 0' constraint in the ErrorNode which + // leads to contradiction with the constraint of 'x * y'. + // Note that the new constraint was bound to a new symbol 'x'. + })"; + std::string Diags; + EXPECT_TRUE(runCheckerOnCodeWithArgs( + Code, LazyAssumeAndCrossCheckArgs, Diags)); + EXPECT_EQ(Diags, + "test.FalsePositiveGenerator:REACHED_WITH_NO_CONTRADICTION\n"); + // Single warning. The second report was invalidated by the visitor. + + // Without enabling the crosscheck-with-z3 both reports are displayed. + std::string Diags2; + EXPECT_TRUE(runCheckerOnCodeWithArgs( + Code, LazyAssumeArgs, Diags2)); + EXPECT_EQ(Diags2, + "test.FalsePositiveGenerator:REACHED_WITH_NO_CONTRADICTION\n" + "test.FalsePositiveGenerator:CAN_BE_TRUE\n"); +} + +} // namespace +} // namespace ento +} // namespace clang