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.nega...@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); _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits