| //===- unittests/Analysis/FlowSensitive/DataflowEnvironmentTest.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 "clang/Analysis/FlowSensitive/DataflowEnvironment.h" |
| #include "TestingSupport.h" |
| #include "clang/AST/DeclCXX.h" |
| #include "clang/ASTMatchers/ASTMatchFinder.h" |
| #include "clang/ASTMatchers/ASTMatchers.h" |
| #include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h" |
| #include "clang/Analysis/FlowSensitive/StorageLocation.h" |
| #include "clang/Analysis/FlowSensitive/Value.h" |
| #include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h" |
| #include "clang/Tooling/Tooling.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include <memory> |
| |
| namespace { |
| |
| using namespace clang; |
| using namespace dataflow; |
| using ::clang::dataflow::test::findValueDecl; |
| using ::clang::dataflow::test::getFieldValue; |
| using ::testing::Contains; |
| using ::testing::IsNull; |
| using ::testing::NotNull; |
| |
| class EnvironmentTest : public ::testing::Test { |
| protected: |
| EnvironmentTest() : DAContext(std::make_unique<WatchedLiteralsSolver>()) {} |
| |
| DataflowAnalysisContext DAContext; |
| }; |
| |
| TEST_F(EnvironmentTest, FlowCondition) { |
| Environment Env(DAContext); |
| auto &A = Env.arena(); |
| |
| EXPECT_TRUE(Env.proves(A.makeLiteral(true))); |
| EXPECT_TRUE(Env.allows(A.makeLiteral(true))); |
| EXPECT_FALSE(Env.proves(A.makeLiteral(false))); |
| EXPECT_FALSE(Env.allows(A.makeLiteral(false))); |
| |
| auto &X = A.makeAtomRef(A.makeAtom()); |
| EXPECT_FALSE(Env.proves(X)); |
| EXPECT_TRUE(Env.allows(X)); |
| |
| Env.assume(X); |
| EXPECT_TRUE(Env.proves(X)); |
| EXPECT_TRUE(Env.allows(X)); |
| |
| auto &NotX = A.makeNot(X); |
| EXPECT_FALSE(Env.proves(NotX)); |
| EXPECT_FALSE(Env.allows(NotX)); |
| } |
| |
| TEST_F(EnvironmentTest, SetAndGetValueOnCfgOmittedNodes) { |
| // Check that we can set a value on an expression that is omitted from the CFG |
| // (see `ignoreCFGOmittedNodes()`), then retrieve that same value from the |
| // expression. This is a regression test; `setValue()` and `getValue()` |
| // previously did not use `ignoreCFGOmittedNodes()` consistently. |
| |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| struct S { |
| int f(); |
| }; |
| void target() { |
| // Method call on a temporary produces an `ExprWithCleanups`. |
| S().f(); |
| (1); |
| } |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++17"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| const ExprWithCleanups *WithCleanups = selectFirst<ExprWithCleanups>( |
| "cleanups", |
| match(exprWithCleanups(hasType(isInteger())).bind("cleanups"), Context)); |
| ASSERT_NE(WithCleanups, nullptr); |
| |
| const ParenExpr *Paren = selectFirst<ParenExpr>( |
| "paren", match(parenExpr(hasType(isInteger())).bind("paren"), Context)); |
| ASSERT_NE(Paren, nullptr); |
| |
| Environment Env(DAContext); |
| IntegerValue *Val1 = |
| cast<IntegerValue>(Env.createValue(Unit->getASTContext().IntTy)); |
| Env.setValue(*WithCleanups, *Val1); |
| EXPECT_EQ(Env.getValue(*WithCleanups), Val1); |
| |
| IntegerValue *Val2 = |
| cast<IntegerValue>(Env.createValue(Unit->getASTContext().IntTy)); |
| Env.setValue(*Paren, *Val2); |
| EXPECT_EQ(Env.getValue(*Paren), Val2); |
| } |
| |
| TEST_F(EnvironmentTest, CreateValueRecursiveType) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| struct Recursive { |
| bool X; |
| Recursive *R; |
| }; |
| // Use both fields to force them to be created with `createValue`. |
| void Usage(Recursive R) { (void)R.X; (void)R.R; } |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = |
| match(qualType(hasDeclaration(recordDecl( |
| hasName("Recursive"), |
| has(fieldDecl(hasName("R")).bind("field-r"))))) |
| .bind("target"), |
| Context); |
| const QualType *TyPtr = selectFirst<QualType>("target", Results); |
| ASSERT_THAT(TyPtr, NotNull()); |
| QualType Ty = *TyPtr; |
| ASSERT_FALSE(Ty.isNull()); |
| |
| const FieldDecl *R = selectFirst<FieldDecl>("field-r", Results); |
| ASSERT_THAT(R, NotNull()); |
| |
| Results = match(functionDecl(hasName("Usage")).bind("fun"), Context); |
| const auto *Fun = selectFirst<FunctionDecl>("fun", Results); |
| ASSERT_THAT(Fun, NotNull()); |
| |
| // Verify that the struct and the field (`R`) with first appearance of the |
| // type is created successfully. |
| Environment Env(DAContext, *Fun); |
| Env.initialize(); |
| auto &SLoc = cast<RecordStorageLocation>(Env.createObject(Ty)); |
| PointerValue *PV = cast_or_null<PointerValue>(getFieldValue(&SLoc, *R, Env)); |
| EXPECT_THAT(PV, NotNull()); |
| } |
| |
| TEST_F(EnvironmentTest, DifferentReferenceLocInJoin) { |
| // This tests the case where the storage location for a reference-type |
| // variable is different for two states being joined. We used to believe this |
| // could not happen and therefore had an assertion disallowing this; this test |
| // exists to demonstrate that we can handle this condition without a failing |
| // assertion. See also the discussion here: |
| // https://discourse.llvm.org/t/70086/6 |
| |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| void f(int &ref) {} |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| const ValueDecl *Ref = findValueDecl(Context, "ref"); |
| |
| Environment Env1(DAContext); |
| StorageLocation &Loc1 = Env1.createStorageLocation(Context.IntTy); |
| Env1.setStorageLocation(*Ref, Loc1); |
| |
| Environment Env2(DAContext); |
| StorageLocation &Loc2 = Env2.createStorageLocation(Context.IntTy); |
| Env2.setStorageLocation(*Ref, Loc2); |
| |
| EXPECT_NE(&Loc1, &Loc2); |
| |
| Environment::ValueModel Model; |
| Environment EnvJoined = |
| Environment::join(Env1, Env2, Model, Environment::DiscardExprState); |
| |
| // Joining environments with different storage locations for the same |
| // declaration results in the declaration being removed from the joined |
| // environment. |
| EXPECT_EQ(EnvJoined.getStorageLocation(*Ref), nullptr); |
| } |
| |
| TEST_F(EnvironmentTest, InitGlobalVarsFun) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| int Global = 0; |
| int Target () { return Global; } |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = |
| match(decl(anyOf(varDecl(hasName("Global")).bind("global"), |
| functionDecl(hasName("Target")).bind("target"))), |
| Context); |
| const auto *Fun = selectFirst<FunctionDecl>("target", Results); |
| const auto *Var = selectFirst<VarDecl>("global", Results); |
| ASSERT_THAT(Fun, NotNull()); |
| ASSERT_THAT(Var, NotNull()); |
| |
| // Verify the global variable is populated when we analyze `Target`. |
| Environment Env(DAContext, *Fun); |
| Env.initialize(); |
| EXPECT_THAT(Env.getValue(*Var), NotNull()); |
| } |
| |
| // Tests that fields mentioned only in default member initializers are included |
| // in the set of tracked fields. |
| TEST_F(EnvironmentTest, IncludeFieldsFromDefaultInitializers) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| struct S { |
| S() {} |
| int X = 3; |
| int Y = X; |
| }; |
| S foo(); |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = match( |
| qualType(hasDeclaration( |
| cxxRecordDecl(hasName("S"), |
| hasMethod(cxxConstructorDecl().bind("target"))) |
| .bind("struct"))) |
| .bind("ty"), |
| Context); |
| const auto *Constructor = selectFirst<FunctionDecl>("target", Results); |
| const auto *Rec = selectFirst<RecordDecl>("struct", Results); |
| const auto QTy = *selectFirst<QualType>("ty", Results); |
| ASSERT_THAT(Constructor, NotNull()); |
| ASSERT_THAT(Rec, NotNull()); |
| ASSERT_FALSE(QTy.isNull()); |
| |
| auto Fields = Rec->fields(); |
| FieldDecl *XDecl = nullptr; |
| for (FieldDecl *Field : Fields) { |
| if (Field->getNameAsString() == "X") { |
| XDecl = Field; |
| break; |
| } |
| } |
| ASSERT_THAT(XDecl, NotNull()); |
| |
| // Verify that the `X` field of `S` is populated when analyzing the |
| // constructor, even though it is not referenced directly in the constructor. |
| Environment Env(DAContext, *Constructor); |
| Env.initialize(); |
| auto &Loc = cast<RecordStorageLocation>(Env.createObject(QTy)); |
| EXPECT_THAT(getFieldValue(&Loc, *XDecl, Env), NotNull()); |
| } |
| |
| TEST_F(EnvironmentTest, InitGlobalVarsFieldFun) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| struct S { int Bar; }; |
| S Global = {0}; |
| int Target () { return Global.Bar; } |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = |
| match(decl(anyOf(varDecl(hasName("Global")).bind("global"), |
| functionDecl(hasName("Target")).bind("target"))), |
| Context); |
| const auto *Fun = selectFirst<FunctionDecl>("target", Results); |
| const auto *GlobalDecl = selectFirst<VarDecl>("global", Results); |
| ASSERT_THAT(Fun, NotNull()); |
| ASSERT_THAT(GlobalDecl, NotNull()); |
| |
| ASSERT_TRUE(GlobalDecl->getType()->isStructureType()); |
| auto GlobalFields = GlobalDecl->getType()->getAsRecordDecl()->fields(); |
| |
| FieldDecl *BarDecl = nullptr; |
| for (FieldDecl *Field : GlobalFields) { |
| if (Field->getNameAsString() == "Bar") { |
| BarDecl = Field; |
| break; |
| } |
| FAIL() << "Unexpected field: " << Field->getNameAsString(); |
| } |
| ASSERT_THAT(BarDecl, NotNull()); |
| |
| // Verify the global variable is populated when we analyze `Target`. |
| Environment Env(DAContext, *Fun); |
| Env.initialize(); |
| const auto *GlobalLoc = |
| cast<RecordStorageLocation>(Env.getStorageLocation(*GlobalDecl)); |
| auto *BarVal = getFieldValue(GlobalLoc, *BarDecl, Env); |
| EXPECT_TRUE(isa<IntegerValue>(BarVal)); |
| } |
| |
| TEST_F(EnvironmentTest, InitGlobalVarsConstructor) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| int Global = 0; |
| struct Target { |
| Target() : Field(Global) {} |
| int Field; |
| }; |
| )cc"; |
| |
| auto Unit = |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = |
| match(decl(anyOf( |
| varDecl(hasName("Global")).bind("global"), |
| cxxConstructorDecl(ofClass(hasName("Target"))).bind("target"))), |
| Context); |
| const auto *Ctor = selectFirst<CXXConstructorDecl>("target", Results); |
| const auto *Var = selectFirst<VarDecl>("global", Results); |
| ASSERT_TRUE(Ctor != nullptr); |
| ASSERT_THAT(Var, NotNull()); |
| |
| // Verify the global variable is populated when we analyze `Target`. |
| Environment Env(DAContext, *Ctor); |
| Env.initialize(); |
| EXPECT_THAT(Env.getValue(*Var), NotNull()); |
| } |
| |
| // Pointers to Members are a tricky case of accessor calls, complicated further |
| // when using templates where the pointer to the member is a template argument. |
| // This is a repro of a failure case seen in the wild. |
| TEST_F(EnvironmentTest, |
| ModelMemberForAccessorUsingMethodPointerThroughTemplate) { |
| using namespace ast_matchers; |
| |
| std::string Code = R"cc( |
| struct S { |
| int accessor() {return member;} |
| |
| int member = 0; |
| }; |
| |
| template <auto method> |
| int Target(S* S) { |
| return (S->*method)(); |
| } |
| |
| // We want to analyze the instantiation of Target for the accessor. |
| int Instantiator () {S S; return Target<&S::accessor>(&S); } |
| )cc"; |
| |
| auto Unit = |
| // C++17 for the simplifying use of auto in the template declaration. |
| tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++17"}); |
| auto &Context = Unit->getASTContext(); |
| |
| ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U); |
| |
| auto Results = match( |
| decl(anyOf(functionDecl(hasName("Target"), isTemplateInstantiation()) |
| .bind("target"), |
| fieldDecl(hasName("member")).bind("member"), |
| recordDecl(hasName("S")).bind("struct"))), |
| Context); |
| const auto *Fun = selectFirst<FunctionDecl>("target", Results); |
| const auto *Struct = selectFirst<RecordDecl>("struct", Results); |
| const auto *Member = selectFirst<FieldDecl>("member", Results); |
| ASSERT_THAT(Fun, NotNull()); |
| ASSERT_THAT(Struct, NotNull()); |
| ASSERT_THAT(Member, NotNull()); |
| |
| // Verify that `member` is modeled for `S` when we analyze |
| // `Target<&S::accessor>`. |
| Environment Env(DAContext, *Fun); |
| Env.initialize(); |
| EXPECT_THAT(DAContext.getModeledFields(QualType(Struct->getTypeForDecl(), 0)), |
| Contains(Member)); |
| } |
| |
| } // namespace |