llvm-project/clang/unittests/Analysis/FlowSensitive/DataflowEnvironmentTest.cpp
Pasquale Riello 49cb170138
[clang][dataflow] Handle CXXInheritedCtorInitExpr in ResultObjectVisitor. (#99616)
`CXXInheritedCtorInitExpr` is another of the node kinds that should be
considered an "original initializer". An assertion failure in
`assert(Children.size() == 1)` happens without this fix.

---------

Co-authored-by: martinboehme <mboehme@google.com>
2024-07-26 15:39:03 +02:00

554 lines
18 KiB
C++

//===- 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/AST/ExprCXX.h"
#include "clang/AST/Stmt.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>
#include <string>
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));
}
// This is a repro of a failure case seen in the wild.
TEST_F(EnvironmentTest, CXXDefaultInitExprResultObjIsWrappedExprResultObj) {
using namespace ast_matchers;
std::string Code = R"cc(
struct Inner {};
struct S {
S() {}
Inner i = {};
};
)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(cxxConstructorDecl(
hasAnyConstructorInitializer(cxxCtorInitializer(
withInitializer(expr().bind("default_init_expr")))))
.bind("ctor"),
Context);
const auto *Constructor = selectFirst<CXXConstructorDecl>("ctor", Results);
const auto *DefaultInit =
selectFirst<CXXDefaultInitExpr>("default_init_expr", Results);
Environment Env(DAContext, *Constructor);
Env.initialize();
EXPECT_EQ(&Env.getResultObjectLocation(*DefaultInit),
&Env.getResultObjectLocation(*DefaultInit->getExpr()));
}
// This test verifies the behavior of `getResultObjectLocation()` in
// scenarios involving inherited constructors.
// Since the specific AST node of interest `CXXConstructorDecl` is implicitly
// generated, we cannot annotate any statements inside of it as we do in tests
// within TransferTest. Thus, the only way to get the right `Environment` is by
// explicitly initializing it as we do in tests within EnvironmentTest.
// This is why this test is not inside TransferTest, where most of the tests for
// `getResultObjectLocation()` are located.
TEST_F(EnvironmentTest, ResultObjectLocationForInheritedCtorInitExpr) {
using namespace ast_matchers;
std::string Code = R"(
struct Base {
Base(int b) {}
};
struct Derived : Base {
using Base::Base;
};
Derived d = Derived(0);
)";
auto Unit =
tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++20"});
auto &Context = Unit->getASTContext();
ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U);
auto Results =
match(cxxConstructorDecl(
hasAnyConstructorInitializer(cxxCtorInitializer(
withInitializer(expr().bind("inherited_ctor_init_expr")))))
.bind("ctor"),
Context);
const auto *Constructor = selectFirst<CXXConstructorDecl>("ctor", Results);
const auto *InheritedCtorInit = selectFirst<CXXInheritedCtorInitExpr>(
"inherited_ctor_init_expr", Results);
EXPECT_EQ(InheritedCtorInit->child_begin(), InheritedCtorInit->child_end());
Environment Env(DAContext, *Constructor);
Env.initialize();
RecordStorageLocation &Loc = Env.getResultObjectLocation(*InheritedCtorInit);
EXPECT_NE(&Loc, nullptr);
EXPECT_EQ(&Loc, Env.getThisPointeeStorageLocation());
}
TEST_F(EnvironmentTest, Stmt) {
using namespace ast_matchers;
std::string Code = R"cc(
struct S { int i; };
void foo() {
S AnS = S{1};
}
)cc";
auto Unit =
tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"});
auto &Context = Unit->getASTContext();
ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U);
auto *DeclStatement = const_cast<DeclStmt *>(selectFirst<DeclStmt>(
"d", match(declStmt(hasSingleDecl(varDecl(hasName("AnS")))).bind("d"),
Context)));
ASSERT_THAT(DeclStatement, NotNull());
auto *Init = (cast<VarDecl>(*DeclStatement->decl_begin()))->getInit();
ASSERT_THAT(Init, NotNull());
// Verify that we can retrieve the result object location for the initializer
// expression when we analyze the DeclStmt for `AnS`.
Environment Env(DAContext, *DeclStatement);
// Don't crash when initializing.
Env.initialize();
// And don't crash when retrieving the result object location.
Env.getResultObjectLocation(*Init);
}
// This is a crash repro.
TEST_F(EnvironmentTest, LambdaCapturingThisInFieldInitializer) {
using namespace ast_matchers;
std::string Code = R"cc(
struct S {
int f{[this]() { return 1; }()};
};
)cc";
auto Unit =
tooling::buildASTFromCodeWithArgs(Code, {"-fsyntax-only", "-std=c++11"});
auto &Context = Unit->getASTContext();
ASSERT_EQ(Context.getDiagnostics().getClient()->getNumErrors(), 0U);
auto *LambdaCallOperator = selectFirst<CXXMethodDecl>(
"method", match(cxxMethodDecl(hasName("operator()"),
ofClass(cxxRecordDecl(isLambda())))
.bind("method"),
Context));
Environment Env(DAContext, *LambdaCallOperator);
// Don't crash when initializing.
Env.initialize();
// And initialize the captured `this` pointee.
ASSERT_NE(nullptr, Env.getThisPointeeStorageLocation());
}
} // namespace