[clang] [clang-tools-extra] [clang]: Propagate `*noreturn` attributes in `CFG` (PR #146355)
Andrey Karlov via cfe-commits
cfe-commits at lists.llvm.org
Wed Jul 23 05:39:23 PDT 2025
https://github.com/negativ updated https://github.com/llvm/llvm-project/pull/146355
>From fc3b77d8c4b5dd264bd746ed997bcea6cddaf389 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 30 Jun 2025 17:05:41 +0300
Subject: [PATCH 01/17] Initial implementation
---
.../bugprone/unchecked-optional-access.cpp | 36 +++++++++++
clang/include/clang/AST/Decl.h | 5 ++
clang/lib/AST/Decl.cpp | 4 ++
clang/lib/Analysis/CFG.cpp | 62 ++++++++++++++++++-
.../TypeErasedDataflowAnalysis.cpp | 4 +-
5 files changed, 107 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index 3167b85f0e024..f4f82199a0bb5 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -141,6 +141,42 @@ void nullable_value_after_swap(BloombergLP::bdlb::NullableValue<int> &opt1, Bloo
}
}
+void assertion_handler_imp() __attribute__((analyzer_noreturn));
+
+void assertion_handler() {
+ do {
+ assertion_handler_imp();
+ } while(0);
+}
+
+void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
+{
+ if (!opt) {
+ assertion_handler();
+ }
+
+ *opt; // no-warning
+}
+
+void abort();
+
+void do_fail() {
+ abort(); // acts like 'abort()' C-function
+}
+
+void invoke_assertion_handler() {
+ do_fail();
+}
+
+void function_calling_well_known_noreturn(const bsl::optional<int>& opt)
+{
+ if (!opt) {
+ invoke_assertion_handler();
+ }
+
+ *opt; // no-warning
+}
+
template <typename T>
void function_template_without_user(const absl::optional<T> &opt) {
opt.value(); // no-warning
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index c4202f1f3d07e..0b27cb7f2cb4e 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -2624,6 +2624,11 @@ class FunctionDecl : public DeclaratorDecl,
/// an attribute on its declaration or its type.
bool isNoReturn() const;
+ /// Determines whether this function is known to never return for CFG
+ /// analysis. Checks for noreturn attributes on the function declaration
+ /// or its type, including 'analyzer_noreturn' attribute.
+ bool isAnalyzerNoReturn() const;
+
/// True if the function was a definition but its body was skipped.
bool hasSkippedBody() const { return FunctionDeclBits.HasSkippedBody; }
void setHasSkippedBody(bool Skipped = true) {
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index 5cdf75d71e4d7..72f63978c085b 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -3603,6 +3603,10 @@ bool FunctionDecl::isNoReturn() const {
return false;
}
+bool FunctionDecl::isAnalyzerNoReturn() const {
+ return isNoReturn() || hasAttr<AnalyzerNoReturnAttr>();
+}
+
bool FunctionDecl::isMemberLikeConstrainedFriend() const {
// C++20 [temp.friend]p9:
// A non-template friend declaration with a requires-clause [or]
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index cf7595952be27..02876e5770c2c 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -2835,8 +2835,37 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
if (!FD->isVariadic())
findConstructionContextsForArguments(C);
- if (FD->isNoReturn() || C->isBuiltinAssumeFalse(*Context))
- NoReturn = true;
+ if (!NoReturn)
+ NoReturn = FD->isAnalyzerNoReturn() || C->isBuiltinAssumeFalse(*Context);
+
+ // Some well-known 'noreturn' functions
+ if (!NoReturn)
+ NoReturn = llvm::StringSwitch<bool>(FD->getQualifiedNameAsString())
+ .Case("BloombergLP::bsls::Assert::invokeHandler", true)
+ .Case("std::terminate", true)
+ .Case("std::abort", true)
+ .Case("exit", true)
+ .Case("abort", true)
+ .Case("panic", true)
+ .Case("error", true)
+ .Case("Assert", true)
+ .Case("ziperr", true)
+ .Case("assfail", true)
+ .Case("db_error", true)
+ .Case("__assert", true)
+ .Case("__assert2", true)
+ .Case("_wassert", true)
+ .Case("__assert_rtn", true)
+ .Case("__assert_fail", true)
+ .Case("dtrace_assfail", true)
+ .Case("yy_fatal_error", true)
+ .Case("_XCAssertionFailureHandler", true)
+ .Case("_DTAssertionFailureHandler", true)
+ .Case("_TSAssertionFailureHandler", true)
+ .Case("__builtin_trap", true)
+ .Case("__builtin_unreachable", true)
+ .Default(false);
+
if (FD->hasAttr<NoThrowAttr>())
AddEHEdge = false;
if (isBuiltinAssumeWithSideEffects(FD->getASTContext(), C) ||
@@ -6308,6 +6337,35 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
}))
return true;
+ auto HasNoReturnCall = [](const CallExpr *CE) {
+ if (!CE)
+ return false;
+
+ auto *FD = CE->getDirectCallee();
+
+ if (!FD)
+ return false;
+
+ auto NoReturnFromCFG = [FD]() {
+ if (!FD->getBody())
+ return false;
+
+ auto CalleeCFG =
+ CFG::buildCFG(FD, FD->getBody(), &FD->getASTContext(), {});
+
+ return CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
+ };
+
+ return FD->isAnalyzerNoReturn() || NoReturnFromCFG();
+ };
+
+ if (llvm::any_of(*Blk, [&](const CFGElement &Elm) {
+ if (std::optional<CFGStmt> StmtElm = Elm.getAs<CFGStmt>())
+ return HasNoReturnCall(dyn_cast<CallExpr>(StmtElm->getStmt()));
+ return false;
+ }))
+ return true;
+
return false;
}
diff --git a/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp b/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
index 1113bbe7f4d9c..c799ca98e4a0d 100644
--- a/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
+++ b/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
@@ -283,7 +283,7 @@ computeBlockInputState(const CFGBlock &Block, AnalysisContext &AC) {
JoinedStateBuilder Builder(AC, JoinBehavior);
for (const CFGBlock *Pred : Preds) {
// Skip if the `Block` is unreachable or control flow cannot get past it.
- if (!Pred || Pred->hasNoReturnElement())
+ if (!Pred || Pred->isInevitablySinking())
continue;
// Skip if `Pred` was not evaluated yet. This could happen if `Pred` has a
@@ -562,7 +562,7 @@ runTypeErasedDataflowAnalysis(
BlockStates[Block->getBlockID()] = std::move(NewBlockState);
// Do not add unreachable successor blocks to `Worklist`.
- if (Block->hasNoReturnElement())
+ if (Block->isInevitablySinking())
continue;
Worklist.enqueueSuccessors(Block);
>From 0b2e72df4761f41849c82070cbb1ec0fc0c18464 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Fri, 4 Jul 2025 18:25:09 +0300
Subject: [PATCH 02/17] Unittests
---
.../TypeErasedDataflowAnalysisTest.cpp | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index 9fb7bebdbe41e..ac4588293bf09 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -49,6 +49,7 @@ using namespace ast_matchers;
using llvm::IsStringMapEntry;
using ::testing::DescribeMatcher;
using ::testing::IsEmpty;
+using ::testing::Not;
using ::testing::NotNull;
using ::testing::Test;
using ::testing::UnorderedElementsAre;
@@ -693,6 +694,87 @@ TEST_F(NoreturnDestructorTest, ConditionalOperatorNestedBranchReturns) {
// FIXME: Called functions at point `p` should contain only "foo".
}
+class AnalyzerNoreturnTest : public 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_test_defs.h", R"(
+ void assertionHandler() __attribute__((analyzer_noreturn));
+
+ void assertionTrampoline() {
+ assertionHandler();
+ }
+
+ void trap() {}
+ )"));
+
+ ASSERT_THAT_ERROR(
+ test::checkDataflow<FunctionCallAnalysis>(
+ AnalysisInputs<FunctionCallAnalysis>(
+ Code, ast_matchers::hasName("target"),
+ [](ASTContext &C, Environment &) {
+ return FunctionCallAnalysis(C);
+ })
+ .withASTBuildArgs({"-fsyntax-only", "-std=c++17"})
+ .withASTBuildVirtualMappedFiles(std::move(FilesContents)),
+ /*VerifyResults=*/
+ [&Expectations](
+ const llvm::StringMap<
+ DataflowAnalysisState<FunctionCallLattice>> &Results,
+ const AnalysisOutputs &) {
+ EXPECT_THAT(Results, Expectations);
+ }),
+ llvm::Succeeded());
+ }
+};
+
+TEST_F(AnalyzerNoreturnTest,
+ Breathing) {
+ std::string Code = R"(
+ #include "noreturn_test_defs.h"
+
+ void target() {
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap"))))));
+}
+
+TEST_F(AnalyzerNoreturnTest, DirectNoReturnCall) {
+ std::string Code = R"(
+ #include "noreturn_test_defs.h"
+
+ void target() {
+ assertionHandler();
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap")))))));
+}
+
+TEST_F(AnalyzerNoreturnTest, IndirectNoReturnCall) {
+ std::string Code = R"(
+ #include "noreturn_test_defs.h"
+
+ void target() {
+ assertionTrampoline();
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap")))))));
+}
+
// Models an analysis that uses flow conditions.
class SpecialBoolAnalysis final
: public DataflowAnalysis<SpecialBoolAnalysis, NoopLattice> {
>From 06cbe1deec97d8756385eaee423d7e3bbb3f51b2 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 7 Jul 2025 12:52:38 +0300
Subject: [PATCH 03/17] Crash fix & PR fixes
---
clang/lib/Analysis/CFG.cpp | 19 +++--
.../Checkers/NoReturnFunctionChecker.cpp | 70 +++++++++++--------
.../TypeErasedDataflowAnalysisTest.cpp | 7 +-
3 files changed, 57 insertions(+), 39 deletions(-)
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index fe99702a35d0d..bc47891216e7a 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -41,6 +41,7 @@
#include "llvm/ADT/APSInt.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallPtrSet.h"
@@ -6317,6 +6318,12 @@ void CFGBlock::printTerminatorJson(raw_ostream &Out, const LangOptions &LO,
// There may be many more reasons why a sink would appear during analysis
// (eg. checkers may generate sinks arbitrarily), but here we only consider
// sinks that would be obvious by looking at the CFG.
+//
+// This function also performs inter-procedural analysis by recursively
+// examining called functions to detect forwarding chains to noreturn
+// functions. When a function is determined to never return through this
+// analysis, it's automatically marked with analyzer_noreturn attribute
+// for caching and future reference.
static bool isImmediateSinkBlock(const CFGBlock *Blk) {
if (Blk->hasNoReturnElement())
return true;
@@ -6327,10 +6334,9 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
// at least for now, but once we have better support for exceptions,
// we'd need to carefully handle the case when the throw is being
// immediately caught.
- if (llvm::any_of(*Blk, [](const CFGElement &Elm) {
+ if (llvm::any_of(*Blk, [](const CFGElement &Elm) -> bool {
if (std::optional<CFGStmt> StmtElm = Elm.getAs<CFGStmt>())
- if (isa<CXXThrowExpr>(StmtElm->getStmt()))
- return true;
+ return isa<CXXThrowExpr>(StmtElm->getStmt());
return false;
}))
return true;
@@ -6339,11 +6345,16 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
if (!CE)
return false;
+ static thread_local llvm::SmallPtrSet<const FunctionDecl *, 32> InProgress;
+
auto *FD = CE->getDirectCallee();
- if (!FD)
+ if (!FD || InProgress.count(FD))
return false;
+ InProgress.insert(FD);
+ auto DoCleanup = llvm::make_scope_exit([&]() { InProgress.erase(FD); });
+
auto NoReturnFromCFG = [FD]() {
if (!FD->getBody())
return false;
diff --git a/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
index 17c3cb4e9e04c..834bd81c2fa21 100644
--- a/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
@@ -50,37 +50,45 @@ void NoReturnFunctionChecker::checkPostCall(const CallEvent &CE,
BuildSinks = getFunctionExtInfo(C->getType()).getNoReturn();
}
- if (!BuildSinks && CE.isGlobalCFunction()) {
- if (const IdentifierInfo *II = CE.getCalleeIdentifier()) {
- // HACK: Some functions are not marked noreturn, and don't return.
- // Here are a few hardwired ones. If this takes too long, we can
- // potentially cache these results.
- BuildSinks
- = llvm::StringSwitch<bool>(StringRef(II->getName()))
- .Case("exit", true)
- .Case("panic", true)
- .Case("error", true)
- .Case("Assert", true)
- // FIXME: This is just a wrapper around throwing an exception.
- // Eventually inter-procedural analysis should handle this easily.
- .Case("ziperr", true)
- .Case("assfail", true)
- .Case("db_error", true)
- .Case("__assert", true)
- .Case("__assert2", true)
- // For the purpose of static analysis, we do not care that
- // this MSVC function will return if the user decides to continue.
- .Case("_wassert", true)
- .Case("__assert_rtn", true)
- .Case("__assert_fail", true)
- .Case("dtrace_assfail", true)
- .Case("yy_fatal_error", true)
- .Case("_XCAssertionFailureHandler", true)
- .Case("_DTAssertionFailureHandler", true)
- .Case("_TSAssertionFailureHandler", true)
- .Default(false);
- }
- }
+ if (!BuildSinks && CE.isGlobalCFunction()) {
+ if (const IdentifierInfo *II = CE.getCalleeIdentifier()) {
+ // HACK: Some functions are not marked noreturn, and don't return.
+ // Here are a few hardwired ones. If this takes too long, we can
+ // potentially cache these results.
+ //
+ // (!) In case of function list update, please also update
+ // CFGBuilder::VisitCallExpr (CFG.cpp)
+ BuildSinks =
+ llvm::StringSwitch<bool>(StringRef(II->getName()))
+ .Case("exit", true)
+ .Case("abort", true)
+ .Case("panic", true)
+ .Case("error", true)
+ .Case("Assert", true)
+ // FIXME: This is just a wrapper around throwing an exception.
+ // Eventually inter-procedural analysis should handle this
+ // easily.
+ .Case("ziperr", true)
+ .Case("assfail", true)
+ .Case("db_error", true)
+ .Case("__assert", true)
+ .Case("__assert2", true)
+ // For the purpose of static analysis, we do not care that
+ // this MSVC function will return if the user decides to
+ // continue.
+ .Case("_wassert", true)
+ .Case("__assert_rtn", true)
+ .Case("__assert_fail", true)
+ .Case("dtrace_assfail", true)
+ .Case("yy_fatal_error", true)
+ .Case("_XCAssertionFailureHandler", true)
+ .Case("_DTAssertionFailureHandler", true)
+ .Case("_TSAssertionFailureHandler", true)
+ .Case("__builtin_trap", true)
+ .Case("__builtin_unreachable", true)
+ .Default(false);
+ }
+ }
if (BuildSinks)
C.generateSink(C.getState(), C.getPredecessor());
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index ac4588293bf09..9150b3e155533 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -699,8 +699,8 @@ class AnalyzerNoreturnTest : public Test {
template <typename Matcher>
void runDataflow(llvm::StringRef Code, Matcher Expectations) {
tooling::FileContentMappings FilesContents;
- FilesContents.push_back(std::make_pair<std::string, std::string>(
- "noreturn_test_defs.h", R"(
+ FilesContents.push_back(
+ std::make_pair<std::string, std::string>("noreturn_test_defs.h", R"(
void assertionHandler() __attribute__((analyzer_noreturn));
void assertionTrampoline() {
@@ -730,8 +730,7 @@ class AnalyzerNoreturnTest : public Test {
}
};
-TEST_F(AnalyzerNoreturnTest,
- Breathing) {
+TEST_F(AnalyzerNoreturnTest, Breathing) {
std::string Code = R"(
#include "noreturn_test_defs.h"
>From 918475d28f3c4b31970c57fdae66e331aea35d3e Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 7 Jul 2025 19:06:07 +0300
Subject: [PATCH 04/17] Fix some test failures
---
clang/lib/Analysis/CFG.cpp | 30 ----------
.../Analysis/FlowSensitive/TransferTest.cpp | 5 +-
.../TypeErasedDataflowAnalysisTest.cpp | 58 +++++--------------
3 files changed, 18 insertions(+), 75 deletions(-)
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index bc47891216e7a..5dcde0052e00c 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -2833,38 +2833,8 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
// (see [expr.call]).
if (!FD->isVariadic())
findConstructionContextsForArguments(C);
-
if (!NoReturn)
NoReturn = FD->isAnalyzerNoReturn() || C->isBuiltinAssumeFalse(*Context);
-
- // Some well-known 'noreturn' functions
- if (!NoReturn)
- NoReturn = llvm::StringSwitch<bool>(FD->getQualifiedNameAsString())
- .Case("BloombergLP::bsls::Assert::invokeHandler", true)
- .Case("std::terminate", true)
- .Case("std::abort", true)
- .Case("exit", true)
- .Case("abort", true)
- .Case("panic", true)
- .Case("error", true)
- .Case("Assert", true)
- .Case("ziperr", true)
- .Case("assfail", true)
- .Case("db_error", true)
- .Case("__assert", true)
- .Case("__assert2", true)
- .Case("_wassert", true)
- .Case("__assert_rtn", true)
- .Case("__assert_fail", true)
- .Case("dtrace_assfail", true)
- .Case("yy_fatal_error", true)
- .Case("_XCAssertionFailureHandler", true)
- .Case("_DTAssertionFailureHandler", true)
- .Case("_TSAssertionFailureHandler", true)
- .Case("__builtin_trap", true)
- .Case("__builtin_unreachable", true)
- .Default(false);
-
if (FD->hasAttr<NoThrowAttr>())
AddEHEdge = false;
if (isBuiltinAssumeWithSideEffects(FD->getASTContext(), C) ||
diff --git a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
index 214aaee9f97f6..1731189d0cace 100644
--- a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
@@ -5950,14 +5950,15 @@ TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
Code,
[](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
ASTContext &ASTCtx) {
- ASSERT_THAT(Results.keys(), UnorderedElementsAre("loop_body"));
+ // target() considered as 'noreturn' by CFG
+ EXPECT_TRUE(Results.keys().empty());
const Environment &LoopBodyEnv =
getEnvironmentAtAnnotation(Results, "loop_body");
const ValueDecl *FooDecl = findValueDecl(ASTCtx, "Foo");
ASSERT_THAT(FooDecl, NotNull());
- auto &LoopBodyFooVal= getFormula(*FooDecl, LoopBodyEnv);
+ auto &LoopBodyFooVal = getFormula(*FooDecl, LoopBodyEnv);
EXPECT_FALSE(LoopBodyEnv.proves(LoopBodyFooVal));
});
}
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index 9150b3e155533..54b63e5606843 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -408,49 +408,6 @@ TEST_F(DiscardExprStateTest, CallWithParenExprTreatedCorrectly) {
EXPECT_NE(CallExpectState.Env.getValue(FnToPtrDecay), nullptr);
}
-struct NonConvergingLattice {
- int State;
-
- bool operator==(const NonConvergingLattice &Other) const {
- return State == Other.State;
- }
-
- LatticeJoinEffect join(const NonConvergingLattice &Other) {
- if (Other.State == 0)
- return LatticeJoinEffect::Unchanged;
- State += Other.State;
- return LatticeJoinEffect::Changed;
- }
-};
-
-class NonConvergingAnalysis
- : public DataflowAnalysis<NonConvergingAnalysis, NonConvergingLattice> {
-public:
- explicit NonConvergingAnalysis(ASTContext &Context)
- : DataflowAnalysis<NonConvergingAnalysis, NonConvergingLattice>(
- Context,
- // Don't apply builtin transfer function.
- DataflowAnalysisOptions{std::nullopt}) {}
-
- static NonConvergingLattice initialElement() { return {0}; }
-
- void transfer(const CFGElement &, NonConvergingLattice &E, Environment &) {
- ++E.State;
- }
-};
-
-TEST_F(DataflowAnalysisTest, NonConvergingAnalysis) {
- std::string Code = R"(
- void target() {
- while(true) {}
- }
- )";
- auto Res = runAnalysis<NonConvergingAnalysis>(
- Code, [](ASTContext &C) { return NonConvergingAnalysis(C); });
- EXPECT_EQ(llvm::toString(Res.takeError()),
- "maximum number of blocks processed");
-}
-
// Regression test for joins of bool-typed lvalue expressions. The first loop
// results in two passes through the code that follows. Each pass results in a
// different `StorageLocation` for the pointee of `v`. Then, the second loop
@@ -774,6 +731,21 @@ TEST_F(AnalyzerNoreturnTest, IndirectNoReturnCall) {
UnorderedElementsAre("trap")))))));
}
+TEST_F(AnalyzerNoreturnTest, InfiniteLoop) {
+ std::string Code = R"(
+ #include "noreturn_test_defs.h"
+
+ void target() {
+ while(true){}
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap")))))));
+}
+
// Models an analysis that uses flow conditions.
class SpecialBoolAnalysis final
: public DataflowAnalysis<SpecialBoolAnalysis, NoopLattice> {
>From a581a5c529f270053f8989499f33a25c7cdd168b Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 14 Jul 2025 16:25:50 +0300
Subject: [PATCH 05/17] Refactoring initial solution
---
clang/include/clang/AST/Decl.h | 5 +++-
clang/include/clang/Basic/Attr.td | 3 +-
clang/lib/AST/Decl.cpp | 10 +++++--
clang/lib/Analysis/CFG.cpp | 48 +++++++++++++++++++++----------
clang/lib/Sema/SemaDeclAttr.cpp | 18 +++++++++++-
5 files changed, 64 insertions(+), 20 deletions(-)
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index cbecf918e8d42..62d8dad48768b 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -2651,7 +2651,10 @@ class FunctionDecl : public DeclaratorDecl,
/// Determines whether this function is known to never return for CFG
/// analysis. Checks for noreturn attributes on the function declaration
/// or its type, including 'analyzer_noreturn' attribute.
- bool isAnalyzerNoReturn() const;
+ ///
+ /// Returns 'std::nullopt' if function declaration has no '*noreturn'
+ /// attributes
+ std::optional<bool> getAnalyzerNoReturn() const;
/// True if the function was a definition but its body was skipped.
bool hasSkippedBody() const { return FunctionDeclBits.HasSkippedBody; }
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 224cb6a32af28..671f9ef6ef159 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -974,7 +974,8 @@ def AnalyzerNoReturn : InheritableAttr {
// vendor namespace, or should it use a vendor namespace specific to the
// analyzer?
let Spellings = [GNU<"analyzer_noreturn">];
- // TODO: Add subject list.
+ let Args = [DefaultBoolArgument<"Value", /*default=*/1, /*fake=*/0>];
+ let Subjects = SubjectList<[Function]>;
let Documentation = [Undocumented];
}
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index d2c79250f5442..8bdfd4cf92fd5 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -3596,8 +3596,14 @@ bool FunctionDecl::isNoReturn() const {
return false;
}
-bool FunctionDecl::isAnalyzerNoReturn() const {
- return isNoReturn() || hasAttr<AnalyzerNoReturnAttr>();
+std::optional<bool> FunctionDecl::getAnalyzerNoReturn() const {
+ if (isNoReturn())
+ return true;
+
+ if (auto *Attr = getAttr<AnalyzerNoReturnAttr>())
+ return Attr->getValue();
+
+ return std::nullopt;
}
bool FunctionDecl::isMemberLikeConstrainedFriend() const {
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index 5dcde0052e00c..09b37cd5864a3 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -41,7 +41,6 @@
#include "llvm/ADT/APSInt.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
-#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallPtrSet.h"
@@ -2833,8 +2832,13 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
// (see [expr.call]).
if (!FD->isVariadic())
findConstructionContextsForArguments(C);
- if (!NoReturn)
- NoReturn = FD->isAnalyzerNoReturn() || C->isBuiltinAssumeFalse(*Context);
+
+ if (!NoReturn) {
+ auto NoRetAttrOpt = FD->getAnalyzerNoReturn();
+ NoReturn =
+ (NoRetAttrOpt && *NoRetAttrOpt) || C->isBuiltinAssumeFalse(*Context);
+ }
+
if (FD->hasAttr<NoThrowAttr>())
AddEHEdge = false;
if (isBuiltinAssumeWithSideEffects(FD->getASTContext(), C) ||
@@ -6311,31 +6315,45 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
}))
return true;
- auto HasNoReturnCall = [](const CallExpr *CE) {
+ auto HasNoReturnCall = [&](const CallExpr *CE) {
if (!CE)
return false;
- static thread_local llvm::SmallPtrSet<const FunctionDecl *, 32> InProgress;
-
auto *FD = CE->getDirectCallee();
- if (!FD || InProgress.count(FD))
+ if (!FD)
return false;
- InProgress.insert(FD);
- auto DoCleanup = llvm::make_scope_exit([&]() { InProgress.erase(FD); });
+ // HACK: we are gonna cache analysis result as implicit `analyzer_noreturn`
+ // attribute
+ auto *MutFD = const_cast<FunctionDecl *>(FD);
+ auto NoRetAttrOpt = FD->getAnalyzerNoReturn();
+ auto NoReturn = false;
- auto NoReturnFromCFG = [FD]() {
- if (!FD->getBody())
- return false;
+ if (!NoRetAttrOpt && FD->getBody()) {
+ // Mark function as `analyzer_noreturn(false)` to:
+ // * prevent infinite recursion in noreturn analysis
+ // * indicate that we've already analyzed(-ing) this function
+ // * serve as a safe default assumption (function may return)
+ MutFD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
+ FD->getASTContext(), false, FD->getLocation()));
auto CalleeCFG =
CFG::buildCFG(FD, FD->getBody(), &FD->getASTContext(), {});
- return CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
- };
+ NoReturn = CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
+
+ // Override to `analyzer_noreturn(true)`
+ if (NoReturn) {
+ MutFD->dropAttr<AnalyzerNoReturnAttr>();
+ MutFD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
+ FD->getASTContext(), NoReturn, FD->getLocation()));
+ }
+
+ } else if (NoRetAttrOpt)
+ NoReturn = *NoRetAttrOpt;
- return FD->isAnalyzerNoReturn() || NoReturnFromCFG();
+ return NoReturn;
};
if (llvm::any_of(*Blk, [&](const CFGElement &Elm) {
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 60a9aee2d41e7..66e4eed31b6c8 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -2060,7 +2060,23 @@ static void handleAnalyzerNoReturnAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
}
}
- D->addAttr(::new (S.Context) AnalyzerNoReturnAttr(S.Context, AL));
+ bool Value = true;
+
+ if (AL.getNumArgs() > 0) {
+ auto *E = AL.getArgAsExpr(0);
+
+ if (S.CheckBooleanCondition(AL.getLoc(), E, true).isInvalid())
+ return;
+
+ if (!E->EvaluateAsBooleanCondition(Value, S.Context, true)) {
+ S.Diag(AL.getLoc(), diag::err_attribute_argument_n_type)
+ << AL << 1 << AANT_ArgumentIntOrBool << E->getSourceRange();
+
+ return;
+ }
+ }
+
+ D->addAttr(::new (S.Context) AnalyzerNoReturnAttr(S.Context, AL, Value));
}
// PS3 PPU-specific.
>From 737e9ca05cd66f46e586909d9cee76e6242fba19 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 14 Jul 2025 17:02:23 +0300
Subject: [PATCH 06/17] Fix tests (hope so)
---
.../Analysis/FlowSensitive/TransferTest.cpp | 15 ++++--
.../TypeErasedDataflowAnalysisTest.cpp | 52 +++++++++++++++++++
2 files changed, 64 insertions(+), 3 deletions(-)
diff --git a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
index 1731189d0cace..d1916673debb7 100644
--- a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
@@ -5940,8 +5940,18 @@ TEST(TransferTest, ForStmtBranchExtendsFlowCondition) {
TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
std::string Code = R"(
void target(bool Foo) {
- for (;;) {
+ unsigned i = 0;
+
+ for (;;++i) {
(void)0;
+
+ // preventing CFG from considering this function
+ // as 'noreturn'
+ if (i == ~0)
+ break;
+ else
+ i = 0;
+
// [[loop_body]]
}
}
@@ -5950,8 +5960,6 @@ TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
Code,
[](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
ASTContext &ASTCtx) {
- // target() considered as 'noreturn' by CFG
- EXPECT_TRUE(Results.keys().empty());
const Environment &LoopBodyEnv =
getEnvironmentAtAnnotation(Results, "loop_body");
@@ -5961,6 +5969,7 @@ TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
auto &LoopBodyFooVal = getFormula(*FooDecl, LoopBodyEnv);
EXPECT_FALSE(LoopBodyEnv.proves(LoopBodyFooVal));
});
+});
}
TEST(TransferTest, ContextSensitiveOptionDisabled) {
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index 54b63e5606843..de3fd4135945a 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -408,6 +408,58 @@ TEST_F(DiscardExprStateTest, CallWithParenExprTreatedCorrectly) {
EXPECT_NE(CallExpectState.Env.getValue(FnToPtrDecay), nullptr);
}
+
+struct NonConvergingLattice {
+ int State;
+
+ bool operator==(const NonConvergingLattice &Other) const {
+ return State == Other.State;
+ }
+
+ LatticeJoinEffect join(const NonConvergingLattice &Other) {
+ if (Other.State == 0)
+ return LatticeJoinEffect::Unchanged;
+ State += Other.State;
+ return LatticeJoinEffect::Changed;
+ }
+};
+
+class NonConvergingAnalysis
+ : public DataflowAnalysis<NonConvergingAnalysis, NonConvergingLattice> {
+public:
+ explicit NonConvergingAnalysis(ASTContext &Context)
+ : DataflowAnalysis<NonConvergingAnalysis, NonConvergingLattice>(
+ Context,
+ // Don't apply builtin transfer function.
+ DataflowAnalysisOptions{std::nullopt}) {}
+
+ static NonConvergingLattice initialElement() { return {0}; }
+
+ void transfer(const CFGElement &, NonConvergingLattice &E, Environment &) {
+ ++E.State;
+ }
+};
+
+TEST_F(DataflowAnalysisTest, NonConvergingAnalysis) {
+ std::string Code = R"(
+ void target() {
+ unsigned i =0;
+ for(;;++i) {
+ // preventing CFG from considering this function
+ // as 'noreturn'
+ if (i == ~0)
+ break;
+ else
+ i = 0;
+ }
+ }
+ )";
+ auto Res = runAnalysis<NonConvergingAnalysis>(
+ Code, [](ASTContext &C) { return NonConvergingAnalysis(C); });
+ EXPECT_EQ(llvm::toString(Res.takeError()),
+ "maximum number of blocks processed");
+}
+
// Regression test for joins of bool-typed lvalue expressions. The first loop
// results in two passes through the code that follows. Each pass results in a
// different `StorageLocation` for the pointee of `v`. Then, the second loop
>From f2f356e0d4356ce23a4314664acfcccb9db06f5e Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Mon, 14 Jul 2025 17:05:53 +0300
Subject: [PATCH 07/17] Rollback some changes
---
.../Checkers/NoReturnFunctionChecker.cpp | 70 ++++++++-----------
1 file changed, 31 insertions(+), 39 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
index 834bd81c2fa21..17c3cb4e9e04c 100644
--- a/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp
@@ -50,45 +50,37 @@ void NoReturnFunctionChecker::checkPostCall(const CallEvent &CE,
BuildSinks = getFunctionExtInfo(C->getType()).getNoReturn();
}
- if (!BuildSinks && CE.isGlobalCFunction()) {
- if (const IdentifierInfo *II = CE.getCalleeIdentifier()) {
- // HACK: Some functions are not marked noreturn, and don't return.
- // Here are a few hardwired ones. If this takes too long, we can
- // potentially cache these results.
- //
- // (!) In case of function list update, please also update
- // CFGBuilder::VisitCallExpr (CFG.cpp)
- BuildSinks =
- llvm::StringSwitch<bool>(StringRef(II->getName()))
- .Case("exit", true)
- .Case("abort", true)
- .Case("panic", true)
- .Case("error", true)
- .Case("Assert", true)
- // FIXME: This is just a wrapper around throwing an exception.
- // Eventually inter-procedural analysis should handle this
- // easily.
- .Case("ziperr", true)
- .Case("assfail", true)
- .Case("db_error", true)
- .Case("__assert", true)
- .Case("__assert2", true)
- // For the purpose of static analysis, we do not care that
- // this MSVC function will return if the user decides to
- // continue.
- .Case("_wassert", true)
- .Case("__assert_rtn", true)
- .Case("__assert_fail", true)
- .Case("dtrace_assfail", true)
- .Case("yy_fatal_error", true)
- .Case("_XCAssertionFailureHandler", true)
- .Case("_DTAssertionFailureHandler", true)
- .Case("_TSAssertionFailureHandler", true)
- .Case("__builtin_trap", true)
- .Case("__builtin_unreachable", true)
- .Default(false);
- }
- }
+ if (!BuildSinks && CE.isGlobalCFunction()) {
+ if (const IdentifierInfo *II = CE.getCalleeIdentifier()) {
+ // HACK: Some functions are not marked noreturn, and don't return.
+ // Here are a few hardwired ones. If this takes too long, we can
+ // potentially cache these results.
+ BuildSinks
+ = llvm::StringSwitch<bool>(StringRef(II->getName()))
+ .Case("exit", true)
+ .Case("panic", true)
+ .Case("error", true)
+ .Case("Assert", true)
+ // FIXME: This is just a wrapper around throwing an exception.
+ // Eventually inter-procedural analysis should handle this easily.
+ .Case("ziperr", true)
+ .Case("assfail", true)
+ .Case("db_error", true)
+ .Case("__assert", true)
+ .Case("__assert2", true)
+ // For the purpose of static analysis, we do not care that
+ // this MSVC function will return if the user decides to continue.
+ .Case("_wassert", true)
+ .Case("__assert_rtn", true)
+ .Case("__assert_fail", true)
+ .Case("dtrace_assfail", true)
+ .Case("yy_fatal_error", true)
+ .Case("_XCAssertionFailureHandler", true)
+ .Case("_DTAssertionFailureHandler", true)
+ .Case("_TSAssertionFailureHandler", true)
+ .Default(false);
+ }
+ }
if (BuildSinks)
C.generateSink(C.getState(), C.getPredecessor());
>From a7c20d4ac3c02ac12363a7fbee2d4b46f80923de Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Tue, 15 Jul 2025 12:50:52 +0300
Subject: [PATCH 08/17] Use canonical declaration for analysis
---
.../bugprone/unchecked-optional-access.cpp | 17 +++---
clang/lib/Analysis/CFG.cpp | 25 +++++----
.../TypeErasedDataflowAnalysisTest.cpp | 52 +++++++++++++++++++
3 files changed, 73 insertions(+), 21 deletions(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index f4f82199a0bb5..c99c6c3a81477 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -143,6 +143,8 @@ void nullable_value_after_swap(BloombergLP::bdlb::NullableValue<int> &opt1, Bloo
void assertion_handler_imp() __attribute__((analyzer_noreturn));
+void assertion_handler();
+
void assertion_handler() {
do {
assertion_handler_imp();
@@ -158,20 +160,15 @@ void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
*opt; // no-warning
}
-void abort();
-
-void do_fail() {
- abort(); // acts like 'abort()' C-function
-}
-
-void invoke_assertion_handler() {
- do_fail();
+// Should be considered as 'noreturn' by CFG
+void no_return_from_cfg() {
+ for(;;) {}
}
-void function_calling_well_known_noreturn(const bsl::optional<int>& opt)
+void function_calling_no_return_from_cfg(const bsl::optional<int>& opt)
{
if (!opt) {
- invoke_assertion_handler();
+ no_return_from_cfg();
}
*opt; // no-warning
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index 09b37cd5864a3..574780e8e5ba1 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -6324,30 +6324,33 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
if (!FD)
return false;
- // HACK: we are gonna cache analysis result as implicit `analyzer_noreturn`
- // attribute
- auto *MutFD = const_cast<FunctionDecl *>(FD);
- auto NoRetAttrOpt = FD->getAnalyzerNoReturn();
+ auto *CanCD = FD->getCanonicalDecl();
+ auto *DefFD = CanCD->getDefinition();
+ auto NoRetAttrOpt = CanCD->getAnalyzerNoReturn();
auto NoReturn = false;
- if (!NoRetAttrOpt && FD->getBody()) {
+ if (!NoRetAttrOpt && DefFD && DefFD->getBody()) {
+ // HACK: we are gonna cache analysis result as implicit
+ // `analyzer_noreturn` attribute
+ auto *MutCD = const_cast<FunctionDecl *>(CanCD);
+
// Mark function as `analyzer_noreturn(false)` to:
// * prevent infinite recursion in noreturn analysis
// * indicate that we've already analyzed(-ing) this function
// * serve as a safe default assumption (function may return)
- MutFD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
- FD->getASTContext(), false, FD->getLocation()));
+ MutCD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
+ CanCD->getASTContext(), false, CanCD->getLocation()));
auto CalleeCFG =
- CFG::buildCFG(FD, FD->getBody(), &FD->getASTContext(), {});
+ CFG::buildCFG(DefFD, DefFD->getBody(), &DefFD->getASTContext(), {});
NoReturn = CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
// Override to `analyzer_noreturn(true)`
if (NoReturn) {
- MutFD->dropAttr<AnalyzerNoReturnAttr>();
- MutFD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
- FD->getASTContext(), NoReturn, FD->getLocation()));
+ MutCD->dropAttr<AnalyzerNoReturnAttr>();
+ MutCD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
+ CanCD->getASTContext(), NoReturn, CanCD->getLocation()));
}
} else if (NoRetAttrOpt)
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index de3fd4135945a..0f51e7a64c9b0 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -718,6 +718,27 @@ class AnalyzerNoreturnTest : public Test {
void trap() {}
)"));
+ FilesContents.push_back(std::make_pair<std::string, std::string>(
+ "noreturn_test_defs_canonical.h", R"(
+ extern void assertionHandler();
+
+ void assertionSecondTrampoline() {
+ assertionHandler();
+ }
+ )"));
+ FilesContents.push_back(std::make_pair<std::string, std::string>(
+ "noreturn_test_defs_noretcfg.h", R"(
+ // will be marged as noreturn by CFG
+ void assertionHandler() {
+ for (;;) {}
+ }
+
+ void assertionTrampoline() {
+ assertionHandler();
+ }
+
+ void trap() {}
+ )"));
ASSERT_THAT_ERROR(
test::checkDataflow<FunctionCallAnalysis>(
@@ -783,6 +804,37 @@ TEST_F(AnalyzerNoreturnTest, IndirectNoReturnCall) {
UnorderedElementsAre("trap")))))));
}
+TEST_F(AnalyzerNoreturnTest, CanonicalDeclCallCheck) {
+ std::string Code = R"(
+ #include "noreturn_test_defs.h"
+ #include "noreturn_test_defs_canonical.h"
+
+ void target() {
+ assertionSecondTrampoline();
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap")))))));
+}
+
+TEST_F(AnalyzerNoreturnTest, NoReturnFromCFGCheck) {
+ std::string Code = R"(
+ #include "noreturn_test_defs_noretcfg.h"
+
+ void target() {
+ assertionTrampoline();
+ trap();
+ // [[p]]
+ }
+ )";
+ runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
+ "p", HoldsFunctionCallLattice(HasCalledFunctions(
+ UnorderedElementsAre("trap")))))));
+}
+
TEST_F(AnalyzerNoreturnTest, InfiniteLoop) {
std::string Code = R"(
#include "noreturn_test_defs.h"
>From 424c067e02a5c67d46ff0875b5b8058d166af0d7 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Tue, 15 Jul 2025 16:10:18 +0300
Subject: [PATCH 09/17] Update clang/lib/Analysis/CFG.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
clang/lib/Analysis/CFG.cpp | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index 574780e8e5ba1..c220a3fd9702d 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -2833,11 +2833,7 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
if (!FD->isVariadic())
findConstructionContextsForArguments(C);
- if (!NoReturn) {
- auto NoRetAttrOpt = FD->getAnalyzerNoReturn();
- NoReturn =
- (NoRetAttrOpt && *NoRetAttrOpt) || C->isBuiltinAssumeFalse(*Context);
- }
+ NoReturn |= FD->getAnalyzerNoReturn().value_or(false) || C->isBuiltinAssumeFalse(*Context);
if (FD->hasAttr<NoThrowAttr>())
AddEHEdge = false;
>From 375480ef02bda609cde9fc18fc5b8242a031cc4f Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Tue, 15 Jul 2025 16:44:21 +0300
Subject: [PATCH 10/17] Update
clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
.../clang-tidy/checkers/bugprone/unchecked-optional-access.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index c99c6c3a81477..67c7e42fe9477 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -154,7 +154,7 @@ void assertion_handler() {
void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
{
if (!opt) {
- assertion_handler();
+ assertion_handler(); // This will be deduced to have an implicit `analyzer_noreturn` attribute.
}
*opt; // no-warning
>From d1d33a3c5cf9e135306ee489b17cce0c24be41dd Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Tue, 15 Jul 2025 16:45:08 +0300
Subject: [PATCH 11/17] Update
clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
.../clang-tidy/checkers/bugprone/unchecked-optional-access.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index 67c7e42fe9477..cc7a5181ded7a 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -157,7 +157,7 @@ void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
assertion_handler(); // This will be deduced to have an implicit `analyzer_noreturn` attribute.
}
- *opt; // no-warning
+ *opt; // no-warning: The previous condition guards this dereference.
}
// Should be considered as 'noreturn' by CFG
>From 64e76898c4ea1aa2070404954a6f472cb1930d6d Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Tue, 15 Jul 2025 16:45:18 +0300
Subject: [PATCH 12/17] Update
clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
.../checkers/bugprone/unchecked-optional-access.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index cc7a5181ded7a..084ec84d02937 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -161,17 +161,17 @@ void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
}
// Should be considered as 'noreturn' by CFG
-void no_return_from_cfg() {
+void halt() {
for(;;) {}
}
void function_calling_no_return_from_cfg(const bsl::optional<int>& opt)
{
if (!opt) {
- no_return_from_cfg();
+ halt();
}
- *opt; // no-warning
+ *opt; // no-warning: The previous condition guards this dereference.
}
template <typename T>
>From 153bb3d4f8d4fc5ed4382b1702ce4519d0fffc7f Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Wed, 23 Jul 2025 14:21:36 +0300
Subject: [PATCH 13/17] Refactoring
---
.../bugprone/UncheckedOptionalAccessCheck.cpp | 6 ++-
clang/include/clang/AST/Decl.h | 23 +++++++----
clang/include/clang/Analysis/CFG.h | 1 +
.../clang/Analysis/FlowSensitive/AdornedCFG.h | 8 ++--
.../Analysis/FlowSensitive/DataflowAnalysis.h | 10 +++--
clang/lib/AST/Decl.cpp | 17 ++++++--
clang/lib/Analysis/CFG.cpp | 41 +++++++++++++------
.../lib/Analysis/FlowSensitive/AdornedCFG.cpp | 15 ++++---
.../TypeErasedDataflowAnalysis.cpp | 4 +-
.../Analysis/FlowSensitive/TestingSupport.h | 9 +++-
.../Analysis/FlowSensitive/TransferTest.cpp | 13 +-----
.../TypeErasedDataflowAnalysisTest.cpp | 36 +++++-----------
12 files changed, 107 insertions(+), 76 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
index 0b51d5677929c..7b48d44bce062 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
@@ -49,6 +49,8 @@ void UncheckedOptionalAccessCheck::check(
const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
if (FuncDecl->isTemplated())
return;
+ CFG::BuildOptions Opts;
+ Opts.ExtendedNoReturnAnalysis = true;
UncheckedOptionalAccessDiagnoser Diagnoser(ModelOptions);
// FIXME: Allow user to set the (defaulted) SAT iterations max for
@@ -56,7 +58,9 @@ void UncheckedOptionalAccessCheck::check(
if (llvm::Expected<llvm::SmallVector<UncheckedOptionalAccessDiagnostic>>
Diags = dataflow::diagnoseFunction<UncheckedOptionalAccessModel,
UncheckedOptionalAccessDiagnostic>(
- *FuncDecl, *Result.Context, Diagnoser))
+ *FuncDecl, *Result.Context, Diagnoser,
+ dataflow::kDefaultMaxSATIterations,
+ dataflow::kDefaultMaxBlockVisits, Opts))
for (const UncheckedOptionalAccessDiagnostic &Diag : *Diags) {
diag(Diag.Range.getBegin(), "unchecked access to optional value")
<< Diag.Range;
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index 1b8f2e86ee602..cebb2f152bcb0 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -2021,6 +2021,18 @@ class FunctionDecl : public DeclaratorDecl,
};
+ /// Three-state classification for function return behavior in CFG analysis:
+ /// undefined (not analyzed), no-return (never returns), or normal return.
+ enum class AnalyzerSinkKind {
+ // Function has no explicit `analyzer_noreturn` or `noreturn` attribute
+ Undefined,
+ // Function never returns control to the caller
+ NoReturn,
+ // Function returns control to the caller (marked with
+ // `analyzer_noreturn(false)`)
+ NoSink
+ };
+
/// Stashed information about a defaulted/deleted function body.
class DefaultedOrDeletedFunctionInfo final
: llvm::TrailingObjects<DefaultedOrDeletedFunctionInfo, DeclAccessPair,
@@ -2668,13 +2680,10 @@ class FunctionDecl : public DeclaratorDecl,
/// an attribute on its declaration or its type.
bool isNoReturn() const;
- /// Determines whether this function is known to never return for CFG
- /// analysis. Checks for noreturn attributes on the function declaration
- /// or its type, including 'analyzer_noreturn' attribute.
- ///
- /// Returns 'std::nullopt' if function declaration has no '*noreturn'
- /// attributes
- std::optional<bool> getAnalyzerNoReturn() const;
+ /// Determines function return behavior
+ AnalyzerSinkKind getAnalyzerSinkKind() const;
+ void setAnalyzerSinkKind(AnalyzerSinkKind kind);
+
/// True if the function was a definition but its body was skipped.
bool hasSkippedBody() const { return FunctionDeclBits.HasSkippedBody; }
diff --git a/clang/include/clang/Analysis/CFG.h b/clang/include/clang/Analysis/CFG.h
index 1b1ff5e558ec5..bef0700259270 100644
--- a/clang/include/clang/Analysis/CFG.h
+++ b/clang/include/clang/Analysis/CFG.h
@@ -1251,6 +1251,7 @@ class CFG {
bool MarkElidedCXXConstructors = false;
bool AddVirtualBaseBranches = false;
bool OmitImplicitValueInitializers = false;
+ bool ExtendedNoReturnAnalysis = false;
BuildOptions() = default;
diff --git a/clang/include/clang/Analysis/FlowSensitive/AdornedCFG.h b/clang/include/clang/Analysis/FlowSensitive/AdornedCFG.h
index 5c64e5b094749..9d505b5f71d63 100644
--- a/clang/include/clang/Analysis/FlowSensitive/AdornedCFG.h
+++ b/clang/include/clang/Analysis/FlowSensitive/AdornedCFG.h
@@ -49,12 +49,14 @@ class AdornedCFG {
/// Builds an `AdornedCFG` from a `FunctionDecl`.
/// `Func.doesThisDeclarationHaveABody()` must be true, and
/// `Func.isTemplated()` must be false.
- static llvm::Expected<AdornedCFG> build(const FunctionDecl &Func);
+ static llvm::Expected<AdornedCFG>
+ build(const FunctionDecl &Func, const CFG::BuildOptions &CfgInitOpts = {});
/// Builds an `AdornedCFG` from an AST node. `D` is the function in which
/// `S` resides. `D.isTemplated()` must be false.
- static llvm::Expected<AdornedCFG> build(const Decl &D, Stmt &S,
- ASTContext &C);
+ static llvm::Expected<AdornedCFG>
+ build(const Decl &D, Stmt &S, ASTContext &C,
+ const CFG::BuildOptions &CfgInitOpts = {});
/// Returns the `Decl` containing the statement used to construct the CFG, if
/// available.
diff --git a/clang/include/clang/Analysis/FlowSensitive/DataflowAnalysis.h b/clang/include/clang/Analysis/FlowSensitive/DataflowAnalysis.h
index e6efde091871f..acd784c70f90b 100644
--- a/clang/include/clang/Analysis/FlowSensitive/DataflowAnalysis.h
+++ b/clang/include/clang/Analysis/FlowSensitive/DataflowAnalysis.h
@@ -322,8 +322,9 @@ llvm::Expected<llvm::SmallVector<Diagnostic>>
diagnoseFunction(const FunctionDecl &FuncDecl, ASTContext &ASTCtx,
DiagnosisCallbacks<AnalysisT, Diagnostic> Diagnoser,
std::int64_t MaxSATIterations = kDefaultMaxSATIterations,
- std::int32_t MaxBlockVisits = kDefaultMaxBlockVisits) {
- llvm::Expected<AdornedCFG> Context = AdornedCFG::build(FuncDecl);
+ std::int32_t MaxBlockVisits = kDefaultMaxBlockVisits,
+ const CFG::BuildOptions &CfgOptions = {}) {
+ llvm::Expected<AdornedCFG> Context = AdornedCFG::build(FuncDecl, CfgOptions);
if (!Context)
return Context.takeError();
@@ -383,10 +384,11 @@ llvm::Expected<llvm::SmallVector<Diagnostic>>
diagnoseFunction(const FunctionDecl &FuncDecl, ASTContext &ASTCtx,
DiagnosisCallback<AnalysisT, Diagnostic> Diagnoser,
std::int64_t MaxSATIterations = kDefaultMaxSATIterations,
- std::int32_t MaxBlockVisits = kDefaultMaxBlockVisits) {
+ std::int32_t MaxBlockVisits = kDefaultMaxBlockVisits,
+ const CFG::BuildOptions &Opts = {}) {
DiagnosisCallbacks<AnalysisT, Diagnostic> Callbacks = {nullptr, Diagnoser};
return diagnoseFunction(FuncDecl, ASTCtx, Callbacks, MaxSATIterations,
- MaxBlockVisits);
+ MaxBlockVisits, Opts);
}
/// Abstract base class for dataflow "models": reusable analysis components that
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index 59e79247f573f..cc8ad83332998 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -3596,14 +3596,23 @@ bool FunctionDecl::isNoReturn() const {
return false;
}
-std::optional<bool> FunctionDecl::getAnalyzerNoReturn() const {
+FunctionDecl::AnalyzerSinkKind FunctionDecl::getAnalyzerSinkKind() const {
if (isNoReturn())
- return true;
+ return AnalyzerSinkKind::NoReturn;
if (auto *Attr = getAttr<AnalyzerNoReturnAttr>())
- return Attr->getValue();
+ return Attr->getValue() ? AnalyzerSinkKind::NoReturn
+ : AnalyzerSinkKind::NoSink;
- return std::nullopt;
+ return AnalyzerSinkKind::Undefined;
+}
+
+void FunctionDecl::setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind kind) {
+ dropAttr<AnalyzerNoReturnAttr>();
+
+ if (kind != AnalyzerSinkKind::Undefined)
+ addAttr(AnalyzerNoReturnAttr::CreateImplicit(
+ getASTContext(), kind == AnalyzerSinkKind::NoReturn, getLocation()));
}
bool FunctionDecl::isMemberLikeConstrainedFriend() const {
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index c220a3fd9702d..cc4dfadba6a02 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -2833,7 +2833,27 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
if (!FD->isVariadic())
findConstructionContextsForArguments(C);
- NoReturn |= FD->getAnalyzerNoReturn().value_or(false) || C->isBuiltinAssumeFalse(*Context);
+ auto SinkKind = FD->getAnalyzerSinkKind();
+ NoReturn |= (SinkKind == FunctionDecl::AnalyzerSinkKind::NoReturn) ||
+ C->isBuiltinAssumeFalse(*Context);
+
+ if (BuildOpts.ExtendedNoReturnAnalysis && !NoReturn &&
+ SinkKind == FunctionDecl::AnalyzerSinkKind::Undefined) {
+ auto *CanCD = FD->getCanonicalDecl();
+ auto *DefFD = CanCD->getDefinition();
+
+ CanCD->setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind::NoSink);
+
+ if (DefFD && DefFD->getBody()) {
+ auto CalleeCFG = CFG::buildCFG(DefFD, DefFD->getBody(),
+ &DefFD->getASTContext(), BuildOpts);
+
+ if (CalleeCFG && CalleeCFG->getEntry().isInevitablySinking()) {
+ CanCD->setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind::NoReturn);
+ NoReturn = true;
+ }
+ }
+ }
if (FD->hasAttr<NoThrowAttr>())
AddEHEdge = false;
@@ -6322,10 +6342,11 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
auto *CanCD = FD->getCanonicalDecl();
auto *DefFD = CanCD->getDefinition();
- auto NoRetAttrOpt = CanCD->getAnalyzerNoReturn();
+ auto NoRetKind = CanCD->getAnalyzerSinkKind();
auto NoReturn = false;
- if (!NoRetAttrOpt && DefFD && DefFD->getBody()) {
+ if ((NoRetKind == FunctionDecl::AnalyzerSinkKind::Undefined) && DefFD &&
+ DefFD->getBody()) {
// HACK: we are gonna cache analysis result as implicit
// `analyzer_noreturn` attribute
auto *MutCD = const_cast<FunctionDecl *>(CanCD);
@@ -6334,8 +6355,7 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
// * prevent infinite recursion in noreturn analysis
// * indicate that we've already analyzed(-ing) this function
// * serve as a safe default assumption (function may return)
- MutCD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
- CanCD->getASTContext(), false, CanCD->getLocation()));
+ MutCD->setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind::NoSink);
auto CalleeCFG =
CFG::buildCFG(DefFD, DefFD->getBody(), &DefFD->getASTContext(), {});
@@ -6343,14 +6363,11 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
NoReturn = CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
// Override to `analyzer_noreturn(true)`
- if (NoReturn) {
- MutCD->dropAttr<AnalyzerNoReturnAttr>();
- MutCD->addAttr(AnalyzerNoReturnAttr::CreateImplicit(
- CanCD->getASTContext(), NoReturn, CanCD->getLocation()));
- }
+ if (NoReturn)
+ MutCD->setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind::NoReturn);
- } else if (NoRetAttrOpt)
- NoReturn = *NoRetAttrOpt;
+ } else
+ NoReturn = (NoRetKind == FunctionDecl::AnalyzerSinkKind::NoReturn);
return NoReturn;
};
diff --git a/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp b/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp
index 6c4847c7c23fb..f98004bf35e77 100644
--- a/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp
+++ b/clang/lib/Analysis/FlowSensitive/AdornedCFG.cpp
@@ -132,17 +132,20 @@ StmtToBlockMap::StmtToBlockMap(const CFG &Cfg)
} // namespace internal
-llvm::Expected<AdornedCFG> AdornedCFG::build(const FunctionDecl &Func) {
+llvm::Expected<AdornedCFG>
+AdornedCFG::build(const FunctionDecl &Func,
+ const CFG::BuildOptions &CfgInitOpts) {
if (!Func.doesThisDeclarationHaveABody())
return llvm::createStringError(
std::make_error_code(std::errc::invalid_argument),
"Cannot analyze function without a body");
- return build(Func, *Func.getBody(), Func.getASTContext());
+ return build(Func, *Func.getBody(), Func.getASTContext(), CfgInitOpts);
}
-llvm::Expected<AdornedCFG> AdornedCFG::build(const Decl &D, Stmt &S,
- ASTContext &C) {
+llvm::Expected<AdornedCFG>
+AdornedCFG::build(const Decl &D, Stmt &S, ASTContext &C,
+ const CFG::BuildOptions &CfgInitOpts) {
if (D.isTemplated())
return llvm::createStringError(
std::make_error_code(std::errc::invalid_argument),
@@ -155,7 +158,9 @@ llvm::Expected<AdornedCFG> AdornedCFG::build(const Decl &D, Stmt &S,
std::make_error_code(std::errc::invalid_argument),
"Can only analyze C++");
- CFG::BuildOptions Options;
+ CFG::BuildOptions Options = CfgInitOpts;
+
+ // Override some options
Options.PruneTriviallyFalseEdges = true;
Options.AddImplicitDtors = true;
Options.AddTemporaryDtors = true;
diff --git a/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp b/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
index c799ca98e4a0d..1113bbe7f4d9c 100644
--- a/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
+++ b/clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp
@@ -283,7 +283,7 @@ computeBlockInputState(const CFGBlock &Block, AnalysisContext &AC) {
JoinedStateBuilder Builder(AC, JoinBehavior);
for (const CFGBlock *Pred : Preds) {
// Skip if the `Block` is unreachable or control flow cannot get past it.
- if (!Pred || Pred->isInevitablySinking())
+ if (!Pred || Pred->hasNoReturnElement())
continue;
// Skip if `Pred` was not evaluated yet. This could happen if `Pred` has a
@@ -562,7 +562,7 @@ runTypeErasedDataflowAnalysis(
BlockStates[Block->getBlockID()] = std::move(NewBlockState);
// Do not add unreachable successor blocks to `Worklist`.
- if (Block->isInevitablySinking())
+ if (Block->hasNoReturnElement())
continue;
Worklist.enqueueSuccessors(Block);
diff --git a/clang/unittests/Analysis/FlowSensitive/TestingSupport.h b/clang/unittests/Analysis/FlowSensitive/TestingSupport.h
index 2ba84373531c6..0032cc6079f93 100644
--- a/clang/unittests/Analysis/FlowSensitive/TestingSupport.h
+++ b/clang/unittests/Analysis/FlowSensitive/TestingSupport.h
@@ -175,6 +175,11 @@ template <typename AnalysisT> struct AnalysisInputs {
return std::move(*this);
}
+ AnalysisInputs<AnalysisT> &&withCfgBuildOptions(CFG::BuildOptions opts) && {
+ CfgBuildOptions = std::move(opts);
+ return std::move(*this);
+ }
+
/// Required. Input code that is analyzed.
llvm::StringRef Code;
/// Required. All functions that match this matcher are analyzed.
@@ -201,6 +206,8 @@ template <typename AnalysisT> struct AnalysisInputs {
std::function<std::unique_ptr<Solver>()> SolverFactory = [] {
return std::make_unique<WatchedLiteralsSolver>();
};
+ /// CFG build options
+ CFG::BuildOptions CfgBuildOptions = {};
};
/// Returns assertions based on annotations that are present after statements in
@@ -288,7 +295,7 @@ checkDataflow(AnalysisInputs<AnalysisT> AI,
llvm::errc::invalid_argument, "Could not find the target function.");
// Build the control flow graph for the target function.
- auto MaybeACFG = AdornedCFG::build(*Target);
+ auto MaybeACFG = AdornedCFG::build(*Target, AI.CfgBuildOptions);
if (!MaybeACFG)
return MaybeACFG.takeError();
auto &ACFG = *MaybeACFG;
diff --git a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
index d1916673debb7..2a428890bd346 100644
--- a/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TransferTest.cpp
@@ -5940,18 +5940,8 @@ TEST(TransferTest, ForStmtBranchExtendsFlowCondition) {
TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
std::string Code = R"(
void target(bool Foo) {
- unsigned i = 0;
-
- for (;;++i) {
+ for (;;) {
(void)0;
-
- // preventing CFG from considering this function
- // as 'noreturn'
- if (i == ~0)
- break;
- else
- i = 0;
-
// [[loop_body]]
}
}
@@ -5969,7 +5959,6 @@ TEST(TransferTest, ForStmtBranchWithoutConditionDoesNotExtendFlowCondition) {
auto &LoopBodyFooVal = getFormula(*FooDecl, LoopBodyEnv);
EXPECT_FALSE(LoopBodyEnv.proves(LoopBodyFooVal));
});
-});
}
TEST(TransferTest, ContextSensitiveOptionDisabled) {
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index 0f51e7a64c9b0..d59efca1aa033 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -443,15 +443,7 @@ class NonConvergingAnalysis
TEST_F(DataflowAnalysisTest, NonConvergingAnalysis) {
std::string Code = R"(
void target() {
- unsigned i =0;
- for(;;++i) {
- // preventing CFG from considering this function
- // as 'noreturn'
- if (i == ~0)
- break;
- else
- i = 0;
- }
+ while(true) {}
}
)";
auto Res = runAnalysis<NonConvergingAnalysis>(
@@ -740,6 +732,9 @@ class AnalyzerNoreturnTest : public Test {
void trap() {}
)"));
+ CFG::BuildOptions Opts;
+ Opts.ExtendedNoReturnAnalysis = true;
+
ASSERT_THAT_ERROR(
test::checkDataflow<FunctionCallAnalysis>(
AnalysisInputs<FunctionCallAnalysis>(
@@ -748,7 +743,8 @@ class AnalyzerNoreturnTest : public Test {
return FunctionCallAnalysis(C);
})
.withASTBuildArgs({"-fsyntax-only", "-std=c++17"})
- .withASTBuildVirtualMappedFiles(std::move(FilesContents)),
+ .withASTBuildVirtualMappedFiles(std::move(FilesContents))
+ .withCfgBuildOptions(std::move(Opts)),
/*VerifyResults=*/
[&Expectations](
const llvm::StringMap<
@@ -784,9 +780,7 @@ TEST_F(AnalyzerNoreturnTest, DirectNoReturnCall) {
// [[p]]
}
)";
- runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
- "p", HoldsFunctionCallLattice(HasCalledFunctions(
- UnorderedElementsAre("trap")))))));
+ runDataflow(Code, IsEmpty());
}
TEST_F(AnalyzerNoreturnTest, IndirectNoReturnCall) {
@@ -799,9 +793,7 @@ TEST_F(AnalyzerNoreturnTest, IndirectNoReturnCall) {
// [[p]]
}
)";
- runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
- "p", HoldsFunctionCallLattice(HasCalledFunctions(
- UnorderedElementsAre("trap")))))));
+ runDataflow(Code, IsEmpty());
}
TEST_F(AnalyzerNoreturnTest, CanonicalDeclCallCheck) {
@@ -815,9 +807,7 @@ TEST_F(AnalyzerNoreturnTest, CanonicalDeclCallCheck) {
// [[p]]
}
)";
- runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
- "p", HoldsFunctionCallLattice(HasCalledFunctions(
- UnorderedElementsAre("trap")))))));
+ runDataflow(Code, IsEmpty());
}
TEST_F(AnalyzerNoreturnTest, NoReturnFromCFGCheck) {
@@ -830,9 +820,7 @@ TEST_F(AnalyzerNoreturnTest, NoReturnFromCFGCheck) {
// [[p]]
}
)";
- runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
- "p", HoldsFunctionCallLattice(HasCalledFunctions(
- UnorderedElementsAre("trap")))))));
+ runDataflow(Code, IsEmpty());
}
TEST_F(AnalyzerNoreturnTest, InfiniteLoop) {
@@ -845,9 +833,7 @@ TEST_F(AnalyzerNoreturnTest, InfiniteLoop) {
// [[p]]
}
)";
- runDataflow(Code, Not(UnorderedElementsAre(IsStringMapEntry(
- "p", HoldsFunctionCallLattice(HasCalledFunctions(
- UnorderedElementsAre("trap")))))));
+ runDataflow(Code, IsEmpty());
}
// Models an analysis that uses flow conditions.
>From fbb33618b32a874b9f02f8f4ac608ee77c4ade48 Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Wed, 23 Jul 2025 14:46:21 +0300
Subject: [PATCH 14/17] code-format & some fixes
---
clang/include/clang/AST/Decl.h | 1 -
clang/include/clang/Basic/Attr.td | 2 +-
.../Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp | 1 -
3 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index cebb2f152bcb0..ad7a0b58510f8 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -2684,7 +2684,6 @@ class FunctionDecl : public DeclaratorDecl,
AnalyzerSinkKind getAnalyzerSinkKind() const;
void setAnalyzerSinkKind(AnalyzerSinkKind kind);
-
/// True if the function was a definition but its body was skipped.
bool hasSkippedBody() const { return FunctionDeclBits.HasSkippedBody; }
void setHasSkippedBody(bool Skipped = true) {
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 671f9ef6ef159..f426f140ef209 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -975,7 +975,7 @@ def AnalyzerNoReturn : InheritableAttr {
// analyzer?
let Spellings = [GNU<"analyzer_noreturn">];
let Args = [DefaultBoolArgument<"Value", /*default=*/1, /*fake=*/0>];
- let Subjects = SubjectList<[Function]>;
+ let Subjects = SubjectList<[Function, ObjCMethod, BlockDecl]>;
let Documentation = [Undocumented];
}
diff --git a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
index d59efca1aa033..7048ac3da9202 100644
--- a/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp
@@ -408,7 +408,6 @@ TEST_F(DiscardExprStateTest, CallWithParenExprTreatedCorrectly) {
EXPECT_NE(CallExpectState.Env.getValue(FnToPtrDecay), nullptr);
}
-
struct NonConvergingLattice {
int State;
>From 5bd4e9e04bf23a87f2fb8634ef491924d46e66fc Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Wed, 23 Jul 2025 14:54:59 +0300
Subject: [PATCH 15/17] Remove BlockDecl
---
clang/include/clang/Basic/Attr.td | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index f426f140ef209..33d89dd610371 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -975,7 +975,7 @@ def AnalyzerNoReturn : InheritableAttr {
// analyzer?
let Spellings = [GNU<"analyzer_noreturn">];
let Args = [DefaultBoolArgument<"Value", /*default=*/1, /*fake=*/0>];
- let Subjects = SubjectList<[Function, ObjCMethod, BlockDecl]>;
+ let Subjects = SubjectList<[Function, ObjCMethod]>;
let Documentation = [Undocumented];
}
>From 2ae2237ce31e01d6d1dc010ca8ead5d8255a669d Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Wed, 23 Jul 2025 15:07:37 +0300
Subject: [PATCH 16/17] Remove subjects
---
clang/include/clang/Basic/Attr.td | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 33d89dd610371..83dca6a25c223 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -975,7 +975,7 @@ def AnalyzerNoReturn : InheritableAttr {
// analyzer?
let Spellings = [GNU<"analyzer_noreturn">];
let Args = [DefaultBoolArgument<"Value", /*default=*/1, /*fake=*/0>];
- let Subjects = SubjectList<[Function, ObjCMethod]>;
+ // let Subjects = SubjectList<[Function, ObjCMethod]>;
let Documentation = [Undocumented];
}
>From 63b012c5a3c884edafec9816cd19d1674febf66c Mon Sep 17 00:00:00 2001
From: Andrey Karlov <dein.negativ at gmail.com>
Date: Wed, 23 Jul 2025 15:39:04 +0300
Subject: [PATCH 17/17] isInevitablySinking takes inter-proc param
---
clang/include/clang/Analysis/CFG.h | 2 +-
clang/lib/Analysis/CFG.cpp | 18 ++++++++++++------
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/clang/include/clang/Analysis/CFG.h b/clang/include/clang/Analysis/CFG.h
index bef0700259270..2d81f86f4e6d6 100644
--- a/clang/include/clang/Analysis/CFG.h
+++ b/clang/include/clang/Analysis/CFG.h
@@ -1080,7 +1080,7 @@ class CFGBlock {
/// Returns true if the block would eventually end with a sink (a noreturn
/// node).
- bool isInevitablySinking() const;
+ bool isInevitablySinking(bool DoInterProcAnalysis = false) const;
CFGTerminator getTerminator() const { return Terminator; }
diff --git a/clang/lib/Analysis/CFG.cpp b/clang/lib/Analysis/CFG.cpp
index cc4dfadba6a02..dd309e963cd20 100644
--- a/clang/lib/Analysis/CFG.cpp
+++ b/clang/lib/Analysis/CFG.cpp
@@ -2848,7 +2848,7 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
auto CalleeCFG = CFG::buildCFG(DefFD, DefFD->getBody(),
&DefFD->getASTContext(), BuildOpts);
- if (CalleeCFG && CalleeCFG->getEntry().isInevitablySinking()) {
+ if (CalleeCFG && CalleeCFG->getEntry().isInevitablySinking(true)) {
CanCD->setAnalyzerSinkKind(FunctionDecl::AnalyzerSinkKind::NoReturn);
NoReturn = true;
}
@@ -6314,7 +6314,8 @@ void CFGBlock::printTerminatorJson(raw_ostream &Out, const LangOptions &LO,
// functions. When a function is determined to never return through this
// analysis, it's automatically marked with analyzer_noreturn attribute
// for caching and future reference.
-static bool isImmediateSinkBlock(const CFGBlock *Blk) {
+static bool isImmediateSinkBlock(const CFGBlock *Blk,
+ bool InterProceduralCheck) {
if (Blk->hasNoReturnElement())
return true;
@@ -6331,6 +6332,9 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
}))
return true;
+ if (!InterProceduralCheck)
+ return false;
+
auto HasNoReturnCall = [&](const CallExpr *CE) {
if (!CE)
return false;
@@ -6360,7 +6364,8 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
auto CalleeCFG =
CFG::buildCFG(DefFD, DefFD->getBody(), &DefFD->getASTContext(), {});
- NoReturn = CalleeCFG && CalleeCFG->getEntry().isInevitablySinking();
+ NoReturn = CalleeCFG && CalleeCFG->getEntry().isInevitablySinking(
+ InterProceduralCheck);
// Override to `analyzer_noreturn(true)`
if (NoReturn)
@@ -6382,11 +6387,11 @@ static bool isImmediateSinkBlock(const CFGBlock *Blk) {
return false;
}
-bool CFGBlock::isInevitablySinking() const {
+bool CFGBlock::isInevitablySinking(bool DoInterProcAnalysis) const {
const CFG &Cfg = *getParent();
const CFGBlock *StartBlk = this;
- if (isImmediateSinkBlock(StartBlk))
+ if (isImmediateSinkBlock(StartBlk, DoInterProcAnalysis))
return true;
llvm::SmallVector<const CFGBlock *, 32> DFSWorkList;
@@ -6406,7 +6411,8 @@ bool CFGBlock::isInevitablySinking() const {
for (const auto &Succ : Blk->succs()) {
if (const CFGBlock *SuccBlk = Succ.getReachableBlock()) {
- if (!isImmediateSinkBlock(SuccBlk) && !Visited.count(SuccBlk)) {
+ if (!isImmediateSinkBlock(SuccBlk, DoInterProcAnalysis) &&
+ !Visited.count(SuccBlk)) {
// If the block has reachable child blocks that aren't no-return,
// add them to the worklist.
DFSWorkList.push_back(SuccBlk);
More information about the cfe-commits
mailing list