This is part of the implementation of the dataflow analysis framework.
See "[RFC] A dataflow analysis framework for Clang AST" on cfe-dev.
Reviewed By: xazax.hun, gribozavr2
Differential Revision: https://reviews.llvm.org/D116022
--- /dev/null
+//===-- ControlFlowContext.h ------------------------------------*- 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 file defines a ControlFlowContext class that is used by dataflow
+// analyses that run over Control-Flow Graphs (CFGs).
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CONTROLFLOWCONTEXT_H
+#define LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CONTROLFLOWCONTEXT_H
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Stmt.h"
+#include "clang/Analysis/CFG.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/Support/Error.h"
+#include <memory>
+#include <utility>
+
+namespace clang {
+namespace dataflow {
+
+/// Holds CFG and other derived context that is needed to perform dataflow
+/// analysis.
+class ControlFlowContext {
+public:
+ /// Builds a ControlFlowContext from an AST node.
+ static llvm::Expected<ControlFlowContext> build(const Decl *D, Stmt *S,
+ ASTContext *C);
+
+ /// Returns the CFG that is stored in this context.
+ const CFG &getCFG() const { return *Cfg; }
+
+ /// Returns a mapping from statements to basic blocks that contain them.
+ const llvm::DenseMap<const Stmt *, const CFGBlock *> &getStmtToBlock() const {
+ return StmtToBlock;
+ }
+
+private:
+ ControlFlowContext(std::unique_ptr<CFG> Cfg,
+ llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock)
+ : Cfg(std::move(Cfg)), StmtToBlock(std::move(StmtToBlock)) {}
+
+ std::unique_ptr<CFG> Cfg;
+ llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock;
+};
+
+} // namespace dataflow
+} // namespace clang
+
+#endif // LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CONTROLFLOWCONTEXT_H
#include "clang/AST/ASTContext.h"
#include "clang/AST/Stmt.h"
#include "clang/Analysis/CFG.h"
+#include "clang/Analysis/FlowSensitive/ControlFlowContext.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.h"
#include "llvm/ADT/Any.h"
/// Performs dataflow analysis and returns a mapping from basic block IDs to
/// dataflow analysis states that model the respective basic blocks. Indices
/// of the returned vector correspond to basic block IDs.
-///
-/// Requirements:
-///
-/// `Cfg` must have been built with `CFG::BuildOptions::setAllAlwaysAdd()` to
-/// ensure that all sub-expressions in a basic block are evaluated.
template <typename AnalysisT>
std::vector<llvm::Optional<DataflowAnalysisState<typename AnalysisT::Lattice>>>
-runDataflowAnalysis(const CFG &Cfg, AnalysisT &Analysis,
+runDataflowAnalysis(const ControlFlowContext &CFCtx, AnalysisT &Analysis,
const Environment &InitEnv) {
auto TypeErasedBlockStates =
- runTypeErasedDataflowAnalysis(Cfg, Analysis, InitEnv);
+ runTypeErasedDataflowAnalysis(CFCtx, Analysis, InitEnv);
std::vector<
llvm::Optional<DataflowAnalysisState<typename AnalysisT::Lattice>>>
BlockStates;
#include "clang/AST/ASTContext.h"
#include "clang/AST/Stmt.h"
#include "clang/Analysis/CFG.h"
+#include "clang/Analysis/FlowSensitive/ControlFlowContext.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
#include "llvm/ADT/Any.h"
/// already been transferred. States in `BlockStates` that are set to
/// `llvm::None` represent basic blocks that are not evaluated yet.
TypeErasedDataflowAnalysisState transferBlock(
+ const ControlFlowContext &CFCtx,
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>> &BlockStates,
const CFGBlock &Block, const Environment &InitEnv,
TypeErasedDataflowAnalysis &Analysis,
/// Performs dataflow analysis and returns a mapping from basic block IDs to
/// dataflow analysis states that model the respective basic blocks. Indices
/// of the returned vector correspond to basic block IDs.
-///
-/// Requirements:
-///
-/// `Cfg` must have been built with `CFG::BuildOptions::setAllAlwaysAdd()` to
-/// ensure that all sub-expressions in a basic block are evaluated.
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>>
-runTypeErasedDataflowAnalysis(const CFG &Cfg,
+runTypeErasedDataflowAnalysis(const ControlFlowContext &CFCtx,
TypeErasedDataflowAnalysis &Analysis,
const Environment &InitEnv);
add_clang_library(clangAnalysisFlowSensitive
+ ControlFlowContext.cpp
TypeErasedDataflowAnalysis.cpp
LINK_LIBS
--- /dev/null
+//===- ControlFlowContext.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
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines a ControlFlowContext class that is used by dataflow
+// analyses that run over Control-Flow Graphs (CFGs).
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Analysis/FlowSensitive/ControlFlowContext.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Stmt.h"
+#include "clang/Analysis/CFG.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/Support/Error.h"
+#include <utility>
+
+namespace clang {
+namespace dataflow {
+
+/// Returns a map from statements to basic blocks that contain them.
+static llvm::DenseMap<const Stmt *, const CFGBlock *>
+buildStmtToBasicBlockMap(const CFG &Cfg) {
+ llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock;
+ for (const CFGBlock *Block : Cfg) {
+ if (Block == nullptr)
+ continue;
+
+ for (const CFGElement &Element : *Block) {
+ auto Stmt = Element.getAs<CFGStmt>();
+ if (!Stmt.hasValue())
+ continue;
+
+ StmtToBlock[Stmt.getValue().getStmt()] = Block;
+ }
+ }
+ return StmtToBlock;
+}
+
+llvm::Expected<ControlFlowContext>
+ControlFlowContext::build(const Decl *D, Stmt *S, ASTContext *C) {
+ CFG::BuildOptions Options;
+ Options.PruneTriviallyFalseEdges = false;
+ Options.AddImplicitDtors = true;
+ Options.AddTemporaryDtors = true;
+ Options.AddInitializers = true;
+
+ // Ensure that all sub-expressions in basic blocks are evaluated.
+ Options.setAllAlwaysAdd();
+
+ auto Cfg = CFG::buildCFG(D, S, C, Options);
+ if (Cfg == nullptr)
+ return llvm::createStringError(
+ std::make_error_code(std::errc::invalid_argument),
+ "CFG::buildCFG failed");
+
+ llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock =
+ buildStmtToBasicBlockMap(*Cfg);
+ return ControlFlowContext(std::move(Cfg), std::move(StmtToBlock));
+}
+
+} // namespace dataflow
+} // namespace clang
/// already been transferred. States in `BlockStates` that are set to
/// `llvm::None` represent basic blocks that are not evaluated yet.
static TypeErasedDataflowAnalysisState computeBlockInputState(
+ const ControlFlowContext &CFCtx,
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>> &BlockStates,
const CFGBlock &Block, const Environment &InitEnv,
TypeErasedDataflowAnalysis &Analysis) {
// the state of each basic block differently.
TypeErasedDataflowAnalysisState State = {Analysis.typeErasedInitialElement(),
InitEnv};
- for (const CFGBlock *Pred : Block.preds()) {
+
+ llvm::DenseSet<const CFGBlock *> Preds;
+ Preds.insert(Block.pred_begin(), Block.pred_end());
+ if (Block.getTerminator().isTemporaryDtorsBranch()) {
+ // This handles a special case where the code that produced the CFG includes
+ // a conditional operator with a branch that constructs a temporary and
+ // calls a destructor annotated as noreturn. The CFG models this as follows:
+ //
+ // B1 (contains the condition of the conditional operator) - succs: B2, B3
+ // B2 (contains code that does not call a noreturn destructor) - succs: B4
+ // B3 (contains code that calls a noreturn destructor) - succs: B4
+ // B4 (has temporary destructor terminator) - succs: B5, B6
+ // B5 (noreturn block that is associated with the noreturn destructor call)
+ // B6 (contains code that follows the conditional operator statement)
+ //
+ // The first successor (B5 above) of a basic block with a temporary
+ // destructor terminator (B4 above) is the block that evaluates the
+ // destructor. If that block has a noreturn element then the predecessor
+ // block that constructed the temporary object (B3 above) is effectively a
+ // noreturn block and its state should not be used as input for the state
+ // of the block that has a temporary destructor terminator (B4 above). This
+ // holds regardless of which branch of the ternary operator calls the
+ // noreturn destructor. However, it doesn't cases where a nested ternary
+ // operator includes a branch that contains a noreturn destructor call.
+ //
+ // See `NoreturnDestructorTest` for concrete examples.
+ if (Block.succ_begin()->getReachableBlock()->hasNoReturnElement()) {
+ auto StmtBlock = CFCtx.getStmtToBlock().find(Block.getTerminatorStmt());
+ assert(StmtBlock != CFCtx.getStmtToBlock().end());
+ Preds.erase(StmtBlock->getSecond());
+ }
+ }
+
+ for (const CFGBlock *Pred : Preds) {
// Skip if the `Block` is unreachable or control flow cannot get past it.
if (!Pred || Pred->hasNoReturnElement())
continue;
}
TypeErasedDataflowAnalysisState transferBlock(
+ const ControlFlowContext &CFCtx,
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>> &BlockStates,
const CFGBlock &Block, const Environment &InitEnv,
TypeErasedDataflowAnalysis &Analysis,
const TypeErasedDataflowAnalysisState &)>
HandleTransferredStmt) {
TypeErasedDataflowAnalysisState State =
- computeBlockInputState(BlockStates, Block, InitEnv, Analysis);
+ computeBlockInputState(CFCtx, BlockStates, Block, InitEnv, Analysis);
for (const CFGElement &Element : Block) {
// FIXME: Evaluate other kinds of `CFGElement`.
const llvm::Optional<CFGStmt> Stmt = Element.getAs<CFGStmt>();
}
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>>
-runTypeErasedDataflowAnalysis(const CFG &Cfg,
+runTypeErasedDataflowAnalysis(const ControlFlowContext &CFCtx,
TypeErasedDataflowAnalysis &Analysis,
const Environment &InitEnv) {
// FIXME: Consider enforcing that `Cfg` meets the requirements that
// are specified in the header. This could be done by remembering
// what options were used to build `Cfg` and asserting on them here.
- PostOrderCFGView POV(&Cfg);
- ForwardDataflowWorklist Worklist(Cfg, &POV);
+ PostOrderCFGView POV(&CFCtx.getCFG());
+ ForwardDataflowWorklist Worklist(CFCtx.getCFG(), &POV);
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>> BlockStates;
- BlockStates.resize(Cfg.size(), llvm::None);
+ BlockStates.resize(CFCtx.getCFG().size(), llvm::None);
// The entry basic block doesn't contain statements so it can be skipped.
- const CFGBlock &Entry = Cfg.getEntry();
+ const CFGBlock &Entry = CFCtx.getCFG().getEntry();
BlockStates[Entry.getBlockID()] = {Analysis.typeErasedInitialElement(),
InitEnv};
Worklist.enqueueSuccessors(&Entry);
const llvm::Optional<TypeErasedDataflowAnalysisState> &OldBlockState =
BlockStates[Block->getBlockID()];
TypeErasedDataflowAnalysisState NewBlockState =
- transferBlock(BlockStates, *Block, InitEnv, Analysis);
+ transferBlock(CFCtx, BlockStates, *Block, InitEnv, Analysis);
if (OldBlockState.hasValue() &&
Analysis.isEqualTypeErased(OldBlockState.getValue().Lattice,
return Result;
}
-
-std::pair<const FunctionDecl *, std::unique_ptr<CFG>>
-test::buildCFG(ASTContext &Context,
- ast_matchers::internal::Matcher<FunctionDecl> FuncMatcher) {
- CFG::BuildOptions Options;
- Options.PruneTriviallyFalseEdges = false;
- Options.AddInitializers = true;
- Options.AddImplicitDtors = true;
- Options.AddTemporaryDtors = true;
- Options.setAllAlwaysAdd();
-
- const FunctionDecl *F = ast_matchers::selectFirst<FunctionDecl>(
- "target",
- ast_matchers::match(
- ast_matchers::functionDecl(ast_matchers::isDefinition(), FuncMatcher)
- .bind("target"),
- Context));
- if (F == nullptr)
- return std::make_pair(nullptr, nullptr);
-
- return std::make_pair(
- F, clang::CFG::buildCFG(F, F->getBody(), &Context, Options));
-}
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchersInternal.h"
#include "clang/Analysis/CFG.h"
+#include "clang/Analysis/FlowSensitive/ControlFlowContext.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Basic/LLVM.h"
+#include "clang/Serialization/PCHContainerOperations.h"
+#include "clang/Tooling/ArgumentsAdjusters.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
buildStatementToAnnotationMapping(const FunctionDecl *Func,
llvm::Annotations AnnotatedCode);
-// Creates a CFG from the body of the function that matches `func_matcher`,
-// suitable to testing a dataflow analysis.
-std::pair<const FunctionDecl *, std::unique_ptr<CFG>>
-buildCFG(ASTContext &Context,
- ast_matchers::internal::Matcher<FunctionDecl> FuncMatcher);
-
// Runs dataflow on the body of the function that matches `func_matcher` in code
// snippet `code`. Requires: `Analysis` contains a type `Lattice`.
template <typename AnalysisT>
using StateT = DataflowAnalysisState<typename AnalysisT::Lattice>;
llvm::Annotations AnnotatedCode(Code);
- auto Unit = tooling::buildASTFromCodeWithArgs(AnnotatedCode.code(), Args);
+ auto Unit = tooling::buildASTFromCodeWithArgs(
+ AnnotatedCode.code(), Args, "input.cc", "clang-dataflow-test",
+ std::make_shared<PCHContainerOperations>(),
+ tooling::getClangStripDependencyFileAdjuster(), VirtualMappedFiles);
auto &Context = Unit->getASTContext();
if (Context.getDiagnostics().getClient()->getNumErrors() != 0) {
"the test log";
}
- std::pair<const FunctionDecl *, std::unique_ptr<CFG>> CFGResult =
- buildCFG(Context, FuncMatcher);
- const auto *F = CFGResult.first;
- auto Cfg = std::move(CFGResult.second);
- ASSERT_TRUE(F != nullptr) << "Could not find target function";
- ASSERT_TRUE(Cfg != nullptr) << "Could not build control flow graph.";
+ const FunctionDecl *F = ast_matchers::selectFirst<FunctionDecl>(
+ "target",
+ ast_matchers::match(
+ ast_matchers::functionDecl(ast_matchers::isDefinition(), FuncMatcher)
+ .bind("target"),
+ Context));
+ ASSERT_TRUE(F != nullptr) << "Could not find target function.";
+
+ auto CFCtx = ControlFlowContext::build(F, F->getBody(), &F->getASTContext());
+ ASSERT_TRUE((bool)CFCtx) << "Could not build ControlFlowContext.";
Environment Env;
auto Analysis = MakeAnalysis(Context, Env);
auto &Annotations = *StmtToAnnotations;
std::vector<llvm::Optional<TypeErasedDataflowAnalysisState>> BlockStates =
- runTypeErasedDataflowAnalysis(*Cfg, Analysis, Env);
+ runTypeErasedDataflowAnalysis(*CFCtx, Analysis, Env);
if (BlockStates.empty()) {
Expectations({}, Context);
// Compute a map from statement annotations to the state computed for
// the program point immediately after the annotated statement.
std::vector<std::pair<std::string, StateT>> Results;
- for (const CFGBlock *Block : *Cfg) {
+ for (const CFGBlock *Block : CFCtx->getCFG()) {
// Skip blocks that were not evaluated.
if (!BlockStates[Block->getBlockID()].hasValue())
continue;
transferBlock(
- BlockStates, *Block, Env, Analysis,
+ *CFCtx, BlockStates, *Block, Env, Analysis,
[&Results, &Annotations](const clang::CFGStmt &Stmt,
const TypeErasedDataflowAnalysisState &State) {
auto It = Annotations.find(Stmt.getStmt());
//
//===----------------------------------------------------------------------===//
+#include "TestingSupport.h"
#include "clang/AST/Decl.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cassert>
#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
#include <vector>
using namespace clang;
using namespace dataflow;
+using ::testing::IsEmpty;
+using ::testing::Pair;
+using ::testing::UnorderedElementsAre;
template <typename AnalysisT>
class AnalysisCallback : public ast_matchers::MatchFinder::MatchCallback {
Stmt *Body = Func->getBody();
assert(Body != nullptr);
- // FIXME: Consider providing a utility that returns a `CFG::BuildOptions`
- // which is a good default for most clients or a utility that directly
- // builds the `CFG` using default `CFG::BuildOptions`.
- CFG::BuildOptions Options;
- Options.AddImplicitDtors = true;
- Options.AddTemporaryDtors = true;
- Options.setAllAlwaysAdd();
-
- std::unique_ptr<CFG> Cfg =
- CFG::buildCFG(nullptr, Body, Result.Context, Options);
- assert(Cfg != nullptr);
+ auto CFCtx = llvm::cantFail(
+ ControlFlowContext::build(nullptr, Body, Result.Context));
AnalysisT Analysis(*Result.Context);
Environment Env;
- BlockStates = runDataflowAnalysis(*Cfg, Analysis, Env);
+ BlockStates = runDataflowAnalysis(CFCtx, Analysis, Env);
}
std::vector<
}
)");
EXPECT_EQ(BlockStates.size(), 4u);
- EXPECT_FALSE(BlockStates[0].hasValue());
+ EXPECT_TRUE(BlockStates[0].hasValue());
EXPECT_TRUE(BlockStates[1].hasValue());
EXPECT_TRUE(BlockStates[2].hasValue());
EXPECT_TRUE(BlockStates[3].hasValue());
}
+
+struct FunctionCallLattice {
+ llvm::SmallSet<std::string, 8> CalledFunctions;
+
+ bool operator==(const FunctionCallLattice &Other) const {
+ return CalledFunctions == Other.CalledFunctions;
+ }
+
+ LatticeJoinEffect join(const FunctionCallLattice &Other) {
+ if (Other.CalledFunctions.empty())
+ return LatticeJoinEffect::Unchanged;
+ const size_t size_before = CalledFunctions.size();
+ CalledFunctions.insert(Other.CalledFunctions.begin(),
+ Other.CalledFunctions.end());
+ return CalledFunctions.size() == size_before ? LatticeJoinEffect::Unchanged
+ : LatticeJoinEffect::Changed;
+ }
+};
+
+std::ostream &operator<<(std::ostream &OS, const FunctionCallLattice &L) {
+ std::string S;
+ llvm::raw_string_ostream ROS(S);
+ llvm::interleaveComma(L.CalledFunctions, ROS);
+ return OS << "{" << S << "}";
+}
+
+class FunctionCallAnalysis
+ : public DataflowAnalysis<FunctionCallAnalysis, FunctionCallLattice> {
+public:
+ explicit FunctionCallAnalysis(ASTContext &Context)
+ : DataflowAnalysis<FunctionCallAnalysis, FunctionCallLattice>(Context) {}
+
+ static FunctionCallLattice initialElement() { return {}; }
+
+ FunctionCallLattice transfer(const Stmt *S, const FunctionCallLattice &E,
+ Environment &Env) {
+ FunctionCallLattice R = E;
+ if (auto *C = dyn_cast<CallExpr>(S)) {
+ if (auto *F = dyn_cast<FunctionDecl>(C->getCalleeDecl())) {
+ R.CalledFunctions.insert(F->getNameInfo().getAsString());
+ }
+ }
+ return R;
+ }
+};
+
+class NoreturnDestructorTest : public ::testing::Test {
+protected:
+ template <typename Matcher>
+ void runDataflow(llvm::StringRef Code, Matcher Expectations) {
+ tooling::FileContentMappings FilesContents;
+ FilesContents.push_back(std::make_pair<std::string, std::string>(
+ "noreturn_destructor_test_defs.h", R"(
+ int foo();
+
+ class Fatal {
+ public:
+ ~Fatal() __attribute__((noreturn));
+ int bar();
+ int baz();
+ };
+
+ class NonFatal {
+ public:
+ ~NonFatal();
+ int bar();
+ };
+ )"));
+
+ test::checkDataflow<FunctionCallAnalysis>(
+ Code, "target",
+ [](ASTContext &C, Environment &) { return FunctionCallAnalysis(C); },
+ [&Expectations](
+ llvm::ArrayRef<std::pair<
+ std::string, DataflowAnalysisState<FunctionCallLattice>>>
+ Results,
+ ASTContext &) { EXPECT_THAT(Results, Expectations); },
+ {"-fsyntax-only", "-std=c++17"}, FilesContents);
+ }
+};
+
+MATCHER_P(HoldsFunctionCallLattice, m,
+ ((negation ? "doesn't hold" : "holds") +
+ llvm::StringRef(" a lattice element that ") +
+ ::testing::DescribeMatcher<FunctionCallLattice>(m, negation))
+ .str()) {
+ return ExplainMatchResult(m, arg.Lattice, result_listener);
+}
+
+MATCHER_P(HasCalledFunctions, m, "") {
+ return ExplainMatchResult(m, arg.CalledFunctions, result_listener);
+}
+
+TEST_F(NoreturnDestructorTest, ConditionalOperatorBothBranchesReturn) {
+ std::string Code = R"(
+ #include "noreturn_destructor_test_defs.h"
+
+ void target(bool b) {
+ int value = b ? foo() : NonFatal().bar();
+ (void)0;
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, UnorderedElementsAre(
+ Pair("p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("foo", "bar"))))));
+}
+
+TEST_F(NoreturnDestructorTest, ConditionalOperatorLeftBranchReturns) {
+ std::string Code = R"(
+ #include "noreturn_destructor_test_defs.h"
+
+ void target(bool b) {
+ int value = b ? foo() : Fatal().bar();
+ (void)0;
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, UnorderedElementsAre(
+ Pair("p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("foo"))))));
+}
+
+TEST_F(NoreturnDestructorTest, ConditionalOperatorRightBranchReturns) {
+ std::string Code = R"(
+ #include "noreturn_destructor_test_defs.h"
+
+ void target(bool b) {
+ int value = b ? Fatal().bar() : foo();
+ (void)0;
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, UnorderedElementsAre(
+ Pair("p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("foo"))))));
+}
+
+TEST_F(NoreturnDestructorTest, ConditionalOperatorNestedBranchesDoNotReturn) {
+ std::string Code = R"(
+ #include "noreturn_destructor_test_defs.h"
+
+ void target(bool b1, bool b2) {
+ int value = b1 ? foo() : (b2 ? Fatal().bar() : Fatal().baz());
+ (void)0;
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, IsEmpty());
+ // FIXME: Called functions at point `p` should contain "foo".
+}
+
+TEST_F(NoreturnDestructorTest, ConditionalOperatorNestedBranchReturns) {
+ std::string Code = R"(
+ #include "noreturn_destructor_test_defs.h"
+
+ void target(bool b1, bool b2) {
+ int value = b1 ? Fatal().bar() : (b2 ? Fatal().baz() : foo());
+ (void)0;
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, UnorderedElementsAre(
+ Pair("p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("baz", "foo"))))));
+ // FIXME: Called functions at point `p` should contain only "foo".
+}