[clang] [clang-tools-extra] `Clang`: `analyzer_noreturn` propagation in `CFG` (PR #146355)
Andrey Karlov via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 30 07:12:22 PDT 2025
https://github.com/negativ created https://github.com/llvm/llvm-project/pull/146355
## Summary
This PR extends `Clang`'s `CFG` analysis to automatically propagate `analyzer_noreturn` attributes through function call chains. When a function exclusively calls no-return functions, it is automatically treated as no-return as well, improving static analysis precision.
## Problem
Currently, simple forwarder functions to no-return calls are not recognized as no-return themselves:
```cpp
void terminate() __attribute__((analyzer_noreturn)) { }
void fatal_error() {
terminate(); // This "never" returns, but fatal_error() is not marked as no-return
}
void handle_error(const std::optional<int> opt) {
if (!opt) {
fatal_error(); // Static analyzer doesn't know this never returns
}
*opt = 1; // False positive: analyzer thinks this is reachable
}
```
## Solution
Enhanced `isImmediateSinkBlock()` in `CFG.cpp` to perform inter-procedural analysis:
1. **Existing behavior preserved**: Direct no-return elements and throw expressions
2. **Enhanced function call analysis**:
- Checks for existing no-return attributes (`analyzer_noreturn`, `noreturn`, `[[noreturn]]`, `_Noreturn`)
- Recognizes well-known C library functions (`exit`, `abort`, `__assert_fail`, etc.)
- Supports namespaced functions (`BloombergLP::bsls::Assert::invokeHandler`, `std::terminate`)
3. **Recursive CFG analysis**: For user-defined functions, builds CFG and checks if all execution paths lead to no-return calls
## Implementation Details
- Modified `isImmediateSinkBlock()` in `lib/Analysis/CFG.cpp`
- Added support for 10+ well-known no-return `C` and `C++` functions
- Inter-procedural analysis limited to functions with available bodies
>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] 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);
More information about the cfe-commits
mailing list