diff options
Diffstat (limited to 'clang/lib/StaticAnalyzer/Checkers/GTestChecker.cpp')
| -rw-r--r-- | clang/lib/StaticAnalyzer/Checkers/GTestChecker.cpp | 298 | 
1 files changed, 298 insertions, 0 deletions
| diff --git a/clang/lib/StaticAnalyzer/Checkers/GTestChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/GTestChecker.cpp new file mode 100644 index 000000000000..f4308f510f0b --- /dev/null +++ b/clang/lib/StaticAnalyzer/Checkers/GTestChecker.cpp @@ -0,0 +1,298 @@ +//==- GTestChecker.cpp - Model gtest API --*- 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 checker models the behavior of un-inlined APIs from the gtest +// unit-testing library to avoid false positives when using assertions from +// that library. +// +//===----------------------------------------------------------------------===// + +#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/AST/Expr.h" +#include "clang/Basic/LangOptions.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace ento; + +// Modeling of un-inlined AssertionResult constructors +// +// The gtest unit testing API provides macros for assertions that expand +// into an if statement that calls a series of constructors and returns +// when the "assertion" is false. +// +// For example, +// +//   ASSERT_TRUE(a == b) +// +// expands into: +// +//   switch (0) +//   case 0: +//   default: +//     if (const ::testing::AssertionResult gtest_ar_ = +//             ::testing::AssertionResult((a == b))) +//       ; +//     else +//       return ::testing::internal::AssertHelper( +//                  ::testing::TestPartResult::kFatalFailure, +//                  "<path to project>", +//                  <line number>, +//                  ::testing::internal::GetBoolAssertionFailureMessage( +//                      gtest_ar_, "a == b", "false", "true") +//                      .c_str()) = ::testing::Message(); +// +// where AssertionResult is defined similarly to +// +//   class AssertionResult { +//   public: +//     AssertionResult(const AssertionResult& other); +//     explicit AssertionResult(bool success) : success_(success) {} +//     operator bool() const { return success_; } +//     ... +//     private: +//     bool success_; +//   }; +// +// In order for the analyzer to correctly handle this assertion, it needs to +// know that the boolean value of the expression "a == b" is stored the +// 'success_' field of the original AssertionResult temporary and propagated +// (via the copy constructor) into the 'success_' field of the object stored +// in 'gtest_ar_'.  That boolean value will then be returned from the bool +// conversion method in the if statement. This guarantees that the assertion +// holds when the return path is not taken. +// +// If the success value is not properly propagated, then the eager case split +// on evaluating the expression can cause pernicious false positives +// on the non-return path: +// +//   ASSERT(ptr != NULL) +//   *ptr = 7; // False positive null pointer dereference here +// +// Unfortunately, the bool constructor cannot be inlined (because its +// implementation is not present in the headers) and the copy constructor is +// not inlined (because it is constructed into a temporary and the analyzer +// does not inline these since it does not yet reliably call temporary +// destructors). +// +// This checker compensates for the missing inlining by propagating the +// _success value across the bool and copy constructors so the assertion behaves +// as expected. + +namespace { +class GTestChecker : public Checker<check::PostCall> { + +  mutable IdentifierInfo *AssertionResultII; +  mutable IdentifierInfo *SuccessII; + +public: +  GTestChecker(); + +  void checkPostCall(const CallEvent &Call, CheckerContext &C) const; + +private: +  void modelAssertionResultBoolConstructor(const CXXConstructorCall *Call, +                                           bool IsRef, CheckerContext &C) const; + +  void modelAssertionResultCopyConstructor(const CXXConstructorCall *Call, +                                           CheckerContext &C) const; + +  void initIdentifierInfo(ASTContext &Ctx) const; + +  SVal +  getAssertionResultSuccessFieldValue(const CXXRecordDecl *AssertionResultDecl, +                                      SVal Instance, +                                      ProgramStateRef State) const; + +  static ProgramStateRef assumeValuesEqual(SVal Val1, SVal Val2, +                                           ProgramStateRef State, +                                           CheckerContext &C); +}; +} // End anonymous namespace. + +GTestChecker::GTestChecker() : AssertionResultII(nullptr), SuccessII(nullptr) {} + +/// Model a call to an un-inlined AssertionResult(bool) or +/// AssertionResult(bool &, ...). +/// To do so, constrain the value of the newly-constructed instance's 'success_' +/// field to be equal to the passed-in boolean value. +/// +/// \param IsRef Whether the boolean parameter is a reference or not. +void GTestChecker::modelAssertionResultBoolConstructor( +    const CXXConstructorCall *Call, bool IsRef, CheckerContext &C) const { +  assert(Call->getNumArgs() >= 1 && Call->getNumArgs() <= 2); + +  ProgramStateRef State = C.getState(); +  SVal BooleanArgVal = Call->getArgSVal(0); +  if (IsRef) { +    // The argument is a reference, so load from it to get the boolean value. +    if (!BooleanArgVal.getAs<Loc>()) +      return; +    BooleanArgVal = C.getState()->getSVal(BooleanArgVal.castAs<Loc>()); +  } + +  SVal ThisVal = Call->getCXXThisVal(); + +  SVal ThisSuccess = getAssertionResultSuccessFieldValue( +      Call->getDecl()->getParent(), ThisVal, State); + +  State = assumeValuesEqual(ThisSuccess, BooleanArgVal, State, C); +  C.addTransition(State); +} + +/// Model a call to an un-inlined AssertionResult copy constructor: +/// +///   AssertionResult(const &AssertionResult other) +/// +/// To do so, constrain the value of the newly-constructed instance's +/// 'success_' field to be equal to the value of the pass-in instance's +/// 'success_' field. +void GTestChecker::modelAssertionResultCopyConstructor( +    const CXXConstructorCall *Call, CheckerContext &C) const { +  assert(Call->getNumArgs() == 1); + +  // The first parameter of the copy constructor must be the other +  // instance to initialize this instances fields from. +  SVal OtherVal = Call->getArgSVal(0); +  SVal ThisVal = Call->getCXXThisVal(); + +  const CXXRecordDecl *AssertResultClassDecl = Call->getDecl()->getParent(); +  ProgramStateRef State = C.getState(); + +  SVal ThisSuccess = getAssertionResultSuccessFieldValue(AssertResultClassDecl, +                                                         ThisVal, State); +  SVal OtherSuccess = getAssertionResultSuccessFieldValue(AssertResultClassDecl, +                                                          OtherVal, State); + +  State = assumeValuesEqual(ThisSuccess, OtherSuccess, State, C); +  C.addTransition(State); +} + +/// Model calls to AssertionResult constructors that are not inlined. +void GTestChecker::checkPostCall(const CallEvent &Call, +                                 CheckerContext &C) const { +  /// If the constructor was inlined, there is no need model it. +  if (C.wasInlined) +    return; + +  initIdentifierInfo(C.getASTContext()); + +  auto *CtorCall = dyn_cast<CXXConstructorCall>(&Call); +  if (!CtorCall) +    return; + +  const CXXConstructorDecl *CtorDecl = CtorCall->getDecl(); +  const CXXRecordDecl *CtorParent = CtorDecl->getParent(); +  if (CtorParent->getIdentifier() != AssertionResultII) +    return; + +  unsigned ParamCount = CtorDecl->getNumParams(); + +  // Call the appropriate modeling method based the parameters and their +  // types. + +  // We have AssertionResult(const &AssertionResult) +  if (CtorDecl->isCopyConstructor() && ParamCount == 1) { +    modelAssertionResultCopyConstructor(CtorCall, C); +    return; +  } + +  // There are two possible boolean constructors, depending on which +  // version of gtest is being used: +  // +  // v1.7 and earlier: +  //      AssertionResult(bool success) +  // +  // v1.8 and greater: +  //      template <typename T> +  //      AssertionResult(const T& success, +  //                      typename internal::EnableIf< +  //                          !internal::ImplicitlyConvertible<T, +  //                              AssertionResult>::value>::type*) +  // +  CanQualType BoolTy = C.getASTContext().BoolTy; +  if (ParamCount == 1 && CtorDecl->getParamDecl(0)->getType() == BoolTy) { +    // We have AssertionResult(bool) +    modelAssertionResultBoolConstructor(CtorCall, /*IsRef=*/false, C); +    return; +  } +  if (ParamCount == 2){ +    auto *RefTy = CtorDecl->getParamDecl(0)->getType()->getAs<ReferenceType>(); +    if (RefTy && +        RefTy->getPointeeType()->getCanonicalTypeUnqualified() == BoolTy) { +      // We have AssertionResult(bool &, ...) +      modelAssertionResultBoolConstructor(CtorCall, /*IsRef=*/true, C); +      return; +    } +  } +} + +void GTestChecker::initIdentifierInfo(ASTContext &Ctx) const { +  if (AssertionResultII) +    return; + +  AssertionResultII = &Ctx.Idents.get("AssertionResult"); +  SuccessII = &Ctx.Idents.get("success_"); +} + +/// Returns the value stored in the 'success_' field of the passed-in +/// AssertionResult instance. +SVal GTestChecker::getAssertionResultSuccessFieldValue( +    const CXXRecordDecl *AssertionResultDecl, SVal Instance, +    ProgramStateRef State) const { + +  DeclContext::lookup_result Result = AssertionResultDecl->lookup(SuccessII); +  if (Result.empty()) +    return UnknownVal(); + +  auto *SuccessField = dyn_cast<FieldDecl>(Result.front()); +  if (!SuccessField) +    return UnknownVal(); + +  Optional<Loc> FieldLoc = +      State->getLValue(SuccessField, Instance).getAs<Loc>(); +  if (!FieldLoc.hasValue()) +    return UnknownVal(); + +  return State->getSVal(*FieldLoc); +} + +/// Constrain the passed-in state to assume two values are equal. +ProgramStateRef GTestChecker::assumeValuesEqual(SVal Val1, SVal Val2, +                                                ProgramStateRef State, +                                                CheckerContext &C) { +  if (!Val1.getAs<DefinedOrUnknownSVal>() || +      !Val2.getAs<DefinedOrUnknownSVal>()) +    return State; + +  auto ValuesEqual = +      C.getSValBuilder().evalEQ(State, Val1.castAs<DefinedOrUnknownSVal>(), +                                Val2.castAs<DefinedOrUnknownSVal>()); + +  if (!ValuesEqual.getAs<DefinedSVal>()) +    return State; + +  State = C.getConstraintManager().assume( +      State, ValuesEqual.castAs<DefinedSVal>(), true); + +  return State; +} + +void ento::registerGTestChecker(CheckerManager &Mgr) { +  Mgr.registerChecker<GTestChecker>(); +} + +bool ento::shouldRegisterGTestChecker(const LangOptions &LO) { +  // gtest is a C++ API so there is no sense running the checker +  // if not compiling for C++. +  return LO.CPlusPlus; +} | 
