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 @@ -6,6 +6,7 @@ add_clang_unittest(StaticAnalysisTests AnalyzerOptionsTest.cpp CallDescriptionTest.cpp + FalsePositiveRefutationBRVisitorTest.cpp StoreTest.cpp RegisterCustomCheckersTest.cpp SymbolReaperTest.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 @@ -77,5 +77,14 @@ return runCheckerOnCode(Code, Diags); } +template +bool runCheckerOnCodeWithArgs(const std::string &Code, std::string &Diags, + const std::vector &Args, + const Twine &FileName = "input.cc") { + llvm::raw_string_ostream OS(Diags); + return tooling::runToolOnCodeWithArgs( + std::make_unique>(OS), Code, Args, FileName); +} + } // 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,192 @@ +//===- 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" + +#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 &, + ProgramStateRef State) const; + CallDescriptionMap Callbacks = { + {{"reachedWithContradiction", 0}, &Self::reachedWithContradiction}, + {{"reachedWithNoContradiction", 0}, &Self::reachedWithNoContradiction}, + {{"reportIfCanBeZero", 1}, &Self::reportIfCanBeZero}, + {{"reportIfCanNotBeZero", 1}, &Self::reportIfCanNotBeZero}, + }; + + 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, + ProgramStateRef State) const { + return report(C, State, "REACHED_WITH_NO_CONTRADICTION"); + } + + bool reachedWithContradiction(const CallEvent &, CheckerContext &C, + ProgramStateRef State) const { + return report(C, State, "REACHED_WITH_CONTRADICTION"); + } + + bool reportIfCanNotBeZero(const CallEvent &Call, CheckerContext &C, + ProgramStateRef State) const { + ProgramStateRef ArgIsZero, ArgIsNotZero; + std::tie(ArgIsZero, ArgIsNotZero) = assumeArgEqZero(Call, C, State); + + if (ArgIsNotZero) + return report(C, ArgIsNotZero, "NON_ZERO_STATE_SHOULD_NOT_EXIST"); + return false; + } + + bool reportIfCanBeZero(const CallEvent &Call, CheckerContext &C, + ProgramStateRef State) const { + ProgramStateRef ArgIsZero, ArgIsNotZero; + std::tie(ArgIsZero, ArgIsNotZero) = assumeArgEqZero(Call, C, State); + + if (ArgIsZero) + return report(C, ArgIsZero, "ZERO_STATE_SHOULD_NOT_EXIST"); + return false; + } + + /* + bool reachIfAssumeOutBoundStateExist(const CallEvent &Call, CheckerContext &C, + ProgramStateRef State) const { + ProgramStateRef OutBound = State->assumeInBound( + Call.getArgSVal(0).castAs(), + Call.getArgSVal(1).castAs(), false); + if (OutBound) + return reached(Call, C, OutBound); + return false; + }*/ + + std::pair + assumeArgEqZero(const CallEvent &Call, CheckerContext &C, + ProgramStateRef State) const { + SValBuilder &SVB = C.getSValBuilder(); + const QualType ArgType = Call.getArgExpr(0)->getType(); + const DefinedOrUnknownSVal Zero = SVB.makeZeroVal(ArgType); + const SVal ArgEqZero = SVB.evalEQ(State, Call.getArgSVal(0), Zero); + return State->assume(ArgEqZero.castAs()); + } + +public: + bool evalCall(const CallEvent &Call, CheckerContext &C) const { + if (const HandlerFn *Callback = Callbacks.lookup(Call)) + return (this->*(*Callback))(Call, C, C.getState()); + 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"); + }); +} + +const std::vector CrossCheckArgs{ + "-Xclang", "-analyzer-config", "-Xclang", "crosscheck-with-z3=true"}; + +bool runFalsePositiveGeneratorOnCode( + const std::string &Code, std::string &Diags, + const std::vector &Args = {}) { + const ::testing::TestInfo *Info = + ::testing::UnitTest::GetInstance()->current_test_info(); + const std::string FileName = (Twine{Info->name()} + ".cc").str(); + + llvm::raw_string_ostream OS(Diags); + return tooling::runToolOnCodeWithArgs( + std::make_unique>(OS), Code, Args, + FileName); +} + +TEST(FalsePositiveRefutationBRVisitor, UnSatInTheMiddleNoReport) { + SKIP_WITHOUT_Z3; + std::string Diags; + constexpr auto Code = R"( + void reachedWithContradiction(); + void test(int x, int y) { + int z = x * y; + if (z == 0) + return; + if (x == 0) + reachedWithContradiction(); // contradiction + // z != 0 => x != 0 && y != 0 => contradict with x == 0 + })"; + EXPECT_TRUE(runFalsePositiveGeneratorOnCode(Code, Diags, CrossCheckArgs)); + EXPECT_EQ(Diags, ""); // no-warning, the report was invalidated by the visitor + + // Without enabling the crosscheck-with-z3 the BugPath is not invalidated. + std::string Diags2; + EXPECT_TRUE(runFalsePositiveGeneratorOnCode(Code, Diags2)); + EXPECT_EQ(Diags2, "test.FalsePositiveGenerator:REACHED_WITH_CONTRADICTION\n"); +} + +TEST(FalsePositiveRefutationBRVisitor, UnSatAtErrorNodeNoReport) { + SKIP_WITHOUT_Z3; + std::string Diags; + constexpr auto Code = R"( + void reportIfCanBeZero(int); + void test(int x, int y) { + int z = x * y; + if (z == 0) + return; + reportIfCanBeZero(x); // contradiction + // The new assumption at the ErrorNode (x == 0) contradict with z != 0. + })"; + + // We have some constraints without contradiction. + // In the error node we tighten a constraint which now contradicts with the + // constraints on the BugPath. The visitor should use the stricter constraint + // on that symbol on crosschecking and invalidate the bugreport. + EXPECT_TRUE(runFalsePositiveGeneratorOnCode(Code, Diags, CrossCheckArgs)); + EXPECT_EQ(Diags, ""); // no-warning, the report was invalidated by the visitor + + // Without enabling the crosscheck-with-z3 the BugPath is not invalidated. + std::string Diags2; + EXPECT_TRUE(runFalsePositiveGeneratorOnCode(Code, Diags2)); + EXPECT_EQ(Diags2, + "test.FalsePositiveGenerator:ZERO_STATE_SHOULD_NOT_EXIST\n"); +} + +} // namespace +} // namespace ento +} // namespace clang