//===- UncheckedOptionalAccessModelTest.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 // //===----------------------------------------------------------------------===// // FIXME: Move this to clang/unittests/Analysis/FlowSensitive/Models. #include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h" #include "TestingSupport.h" #include "clang/AST/ASTContext.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Analysis/FlowSensitive/SourceLocationsLattice.h" #include "clang/Tooling/Tooling.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Error.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include #include using namespace clang; using namespace dataflow; using namespace test; using ::testing::Pair; using ::testing::UnorderedElementsAre; // FIXME: Move header definitions in separate file(s). static constexpr char StdTypeTraitsHeader[] = R"( #ifndef TYPE_TRAITS_H #define TYPE_TRAITS_H namespace std { typedef decltype(sizeof(char)) size_t; template struct integral_constant { static constexpr T value = V; }; using true_type = integral_constant; using false_type = integral_constant; template< class T > struct remove_reference {typedef T type;}; template< class T > struct remove_reference {typedef T type;}; template< class T > struct remove_reference {typedef T type;}; template using remove_reference_t = typename remove_reference::type; template struct remove_extent { typedef T type; }; template struct remove_extent { typedef T type; }; template struct remove_extent { typedef T type; }; template struct is_array : false_type {}; template struct is_array : true_type {}; template struct is_array : true_type {}; template struct is_function : false_type {}; template struct is_function : true_type {}; namespace detail { template struct type_identity { using type = T; }; // or use type_identity (since C++20) template auto try_add_pointer(int) -> type_identity::type*>; template auto try_add_pointer(...) -> type_identity; } // namespace detail template struct add_pointer : decltype(detail::try_add_pointer(0)) {}; template struct conditional { typedef T type; }; template struct conditional { typedef F type; }; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct decay { private: typedef typename remove_reference::type U; public: typedef typename conditional< is_array::value, typename remove_extent::type*, typename conditional::value, typename add_pointer::type, typename remove_cv::type>::type>::type type; }; } // namespace std #endif // TYPE_TRAITS_H )"; static constexpr char StdUtilityHeader[] = R"( #ifndef UTILITY_H #define UTILITY_H #include "std_type_traits.h" namespace std { template constexpr remove_reference_t&& move(T&& x); } // namespace std #endif // UTILITY_H )"; static constexpr char StdInitializerListHeader[] = R"( #ifndef INITIALIZER_LIST_H #define INITIALIZER_LIST_H namespace std { template class initializer_list { public: initializer_list() noexcept; }; } // namespace std #endif // INITIALIZER_LIST_H )"; static constexpr char StdOptionalHeader[] = R"( #include "std_initializer_list.h" #include "std_type_traits.h" #include "std_utility.h" namespace std { template class optional { public: constexpr optional() noexcept; const T& operator*() const&; T& operator*() &; const T&& operator*() const&&; T&& operator*() &&; const T* operator->() const; T* operator->(); const T& value() const&; T& value() &; const T&& value() const&&; T&& value() &&; template constexpr T value_or(U&& v) const&; template T value_or(U&& v) &&; template T& emplace(Args&&... args); template T& emplace(std::initializer_list ilist, Args&&... args); void reset() noexcept; constexpr explicit operator bool() const noexcept; constexpr bool has_value() const noexcept; }; template constexpr optional::type> make_optional(T&& v); template constexpr optional make_optional(Args&&... args); template constexpr optional make_optional(std::initializer_list il, Args&&... args); } // namespace std )"; static constexpr char AbslOptionalHeader[] = R"( #include "std_initializer_list.h" #include "std_type_traits.h" #include "std_utility.h" namespace absl { template class optional { public: constexpr optional() noexcept; const T& operator*() const&; T& operator*() &; const T&& operator*() const&&; T&& operator*() &&; const T* operator->() const; T* operator->(); const T& value() const&; T& value() &; const T&& value() const&&; T&& value() &&; template constexpr T value_or(U&& v) const&; template T value_or(U&& v) &&; template T& emplace(Args&&... args); template T& emplace(std::initializer_list ilist, Args&&... args); void reset() noexcept; constexpr explicit operator bool() const noexcept; constexpr bool has_value() const noexcept; }; template constexpr optional::type> make_optional(T&& v); template constexpr optional make_optional(Args&&... args); template constexpr optional make_optional(std::initializer_list il, Args&&... args); } // namespace absl )"; static constexpr char BaseOptionalHeader[] = R"( #include "std_initializer_list.h" #include "std_type_traits.h" #include "std_utility.h" namespace base { template class Optional { public: constexpr Optional() noexcept; const T& operator*() const&; T& operator*() &; const T&& operator*() const&&; T&& operator*() &&; const T* operator->() const; T* operator->(); const T& value() const&; T& value() &; const T&& value() const&&; T&& value() &&; template constexpr T value_or(U&& v) const&; template T value_or(U&& v) &&; template T& emplace(Args&&... args); template T& emplace(std::initializer_list ilist, Args&&... args); void reset() noexcept; constexpr explicit operator bool() const noexcept; constexpr bool has_value() const noexcept; }; template constexpr Optional::type> make_optional(T&& v); template constexpr Optional make_optional(Args&&... args); template constexpr Optional make_optional(std::initializer_list il, Args&&... args); } // namespace base )"; /// Converts `L` to string. static std::string ConvertToString(const SourceLocationsLattice &L, const ASTContext &Ctx) { return L.getSourceLocations().empty() ? "safe" : "unsafe: " + DebugString(L, Ctx); } /// Replaces all occurrences of `Pattern` in `S` with `Replacement`. static void ReplaceAllOccurrences(std::string &S, const std::string &Pattern, const std::string &Replacement) { size_t Pos = 0; while (true) { Pos = S.find(Pattern, Pos); if (Pos == std::string::npos) break; S.replace(Pos, Pattern.size(), Replacement); } } struct OptionalTypeIdentifier { std::string NamespaceName; std::string TypeName; }; class UncheckedOptionalAccessTest : public ::testing::TestWithParam { protected: template void ExpectLatticeChecksFor(std::string SourceCode, LatticeChecksMatcher MatchesLatticeChecks) { ExpectLatticeChecksFor(SourceCode, ast_matchers::hasName("target"), MatchesLatticeChecks); } private: template void ExpectLatticeChecksFor(std::string SourceCode, FuncDeclMatcher FuncMatcher, LatticeChecksMatcher MatchesLatticeChecks) { ReplaceAllOccurrences(SourceCode, "$ns", GetParam().NamespaceName); ReplaceAllOccurrences(SourceCode, "$optional", GetParam().TypeName); std::vector> Headers; Headers.emplace_back("std_initializer_list.h", StdInitializerListHeader); Headers.emplace_back("std_type_traits.h", StdTypeTraitsHeader); Headers.emplace_back("std_utility.h", StdUtilityHeader); Headers.emplace_back("std_optional.h", StdOptionalHeader); Headers.emplace_back("absl_optional.h", AbslOptionalHeader); Headers.emplace_back("base_optional.h", BaseOptionalHeader); Headers.emplace_back("unchecked_optional_access_test.h", R"( #include "absl_optional.h" #include "base_optional.h" #include "std_initializer_list.h" #include "std_optional.h" #include "std_utility.h" template T Make(); )"); const tooling::FileContentMappings FileContents(Headers.begin(), Headers.end()); llvm::Error Error = checkDataflow( SourceCode, FuncMatcher, [](ASTContext &Ctx, Environment &) { return UncheckedOptionalAccessModel(Ctx); }, [&MatchesLatticeChecks]( llvm::ArrayRef>> CheckToLatticeMap, ASTContext &Ctx) { // FIXME: Consider using a matcher instead of translating // `CheckToLatticeMap` to `CheckToStringifiedLatticeMap`. std::vector> CheckToStringifiedLatticeMap; for (const auto &E : CheckToLatticeMap) { CheckToStringifiedLatticeMap.emplace_back( E.first, ConvertToString(E.second.Lattice, Ctx)); } EXPECT_THAT(CheckToStringifiedLatticeMap, MatchesLatticeChecks); }, {"-fsyntax-only", "-std=c++17", "-Wno-undefined-inline"}, FileContents); if (Error) FAIL() << llvm::toString(std::move(Error)); } }; INSTANTIATE_TEST_SUITE_P( UncheckedOptionalUseTestInst, UncheckedOptionalAccessTest, ::testing::Values(OptionalTypeIdentifier{"std", "optional"}, OptionalTypeIdentifier{"absl", "optional"}, OptionalTypeIdentifier{"base", "Optional"}), [](const ::testing::TestParamInfo &Info) { return Info.param.NamespaceName; }); TEST_P(UncheckedOptionalAccessTest, EmptyFunctionBody) { ExpectLatticeChecksFor(R"( void target() { (void)0; /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); } TEST_P(UncheckedOptionalAccessTest, UnwrapUsingValueNoCheck) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:7"))); ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { std::move(opt).value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:7"))); } TEST_P(UncheckedOptionalAccessTest, UnwrapUsingOperatorStarNoCheck) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { *opt; /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:8"))); ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { *std::move(opt); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:8"))); } TEST_P(UncheckedOptionalAccessTest, UnwrapUsingOperatorArrowNoCheck) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" struct Foo { void foo(); }; void target($ns::$optional opt) { opt->foo(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:9:7"))); ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" struct Foo { void foo(); }; void target($ns::$optional opt) { std::move(opt)->foo(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:9:7"))); } TEST_P(UncheckedOptionalAccessTest, HasValueCheck) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { if (opt.has_value()) { opt.value(); /*[[check]]*/ } } )", UnorderedElementsAre(Pair("check", "safe"))); } TEST_P(UncheckedOptionalAccessTest, OperatorBoolCheck) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { if (opt) { opt.value(); /*[[check]]*/ } } )", UnorderedElementsAre(Pair("check", "safe"))); } TEST_P(UncheckedOptionalAccessTest, UnwrapFunctionCallResultNoCheck) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target() { Make<$ns::$optional>().value(); (void)0; /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:7"))); ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target($ns::$optional opt) { std::move(opt).value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:5:7"))); } TEST_P(UncheckedOptionalAccessTest, DefaultConstructor) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional opt; opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:6:7"))); } TEST_P(UncheckedOptionalAccessTest, MakeOptional) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional opt = $ns::make_optional(0); opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" struct Foo { Foo(int, int); }; void target() { $ns::$optional opt = $ns::make_optional(21, 22); opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" struct Foo { constexpr Foo(std::initializer_list); }; void target() { char a = 'a'; $ns::$optional opt = $ns::make_optional({a}); opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); } TEST_P(UncheckedOptionalAccessTest, ValueOr) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional opt; opt.value_or(0); (void)0; /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); } TEST_P(UncheckedOptionalAccessTest, Emplace) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional opt; opt.emplace(0); opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" void target($ns::$optional *opt) { opt->emplace(0); opt->value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "safe"))); // FIXME: Add tests that call `emplace` in conditional branches. } TEST_P(UncheckedOptionalAccessTest, Reset) { ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional opt = $ns::make_optional(0); opt.reset(); opt.value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:7:7"))); ExpectLatticeChecksFor( R"( #include "unchecked_optional_access_test.h" void target() { $ns::$optional *opt; *opt = $ns::make_optional(0); opt->reset(); opt->value(); /*[[check]]*/ } )", UnorderedElementsAre(Pair("check", "unsafe: input.cc:8:7"))); // FIXME: Add tests that call `reset` in conditional branches. } // FIXME: Add support for: // - constructors (copy, move, non-standard) // - assignment operators (default, copy, move, non-standard) // - swap // - invalidation (passing optional by non-const reference/pointer) // - `value_or(nullptr) != nullptr`, `value_or(0) != 0`, `value_or("").empty()` // - nested `optional` values