Skip to content

Commit df95dfc

Browse files
authored
[clang]: Support analyzer_noreturn attribute in CFG (#150952)
## Problem Currently, functions with `analyzer_noreturn` attribute aren't recognized as `no-return` by `CFG`: ```cpp void assertion_handler() __attribute__((analyzer_noreturn)) { log(...); } void handle_error(const std::optional<int> opt) { if (!opt) { fatal_error(); // Static analyzer doesn't know this never returns } *opt = 1; // False-positive `unchecked-optional-access` warning as analyzer thinks this is reachable } ``` ## Solution 1. Extend the `FunctionDecl` class by adding an `isAnalyzerNoReturn()` function 2. Update `CFGBuilder::VisitCallExpr` to check both `FD->isNoReturn()` and `FD->isAnalyzerNoReturn()` properties ## Comments This PR incorporates part of the work done in #146355
1 parent dc5cf01 commit df95dfc

File tree

5 files changed

+95
-1
lines changed

5 files changed

+95
-1
lines changed

clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ void nullable_value_after_swap(BloombergLP::bdlb::NullableValue<int> &opt1, Bloo
141141
}
142142
}
143143

144+
void assertion_handler() __attribute__((analyzer_noreturn));
145+
146+
void function_calling_analyzer_noreturn(const bsl::optional<int>& opt)
147+
{
148+
if (!opt) {
149+
assertion_handler();
150+
}
151+
152+
*opt; // no-warning: The previous condition guards this dereference.
153+
}
154+
144155
template <typename T>
145156
void function_template_without_user(const absl::optional<T> &opt) {
146157
opt.value(); // no-warning

clang/include/clang/AST/Decl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,6 +2668,10 @@ class FunctionDecl : public DeclaratorDecl,
26682668
/// an attribute on its declaration or its type.
26692669
bool isNoReturn() const;
26702670

2671+
/// Determines whether this function is known to be 'noreturn' for analyzer,
2672+
/// through an `analyzer_noreturn` attribute on its declaration.
2673+
bool isAnalyzerNoReturn() const;
2674+
26712675
/// True if the function was a definition but its body was skipped.
26722676
bool hasSkippedBody() const { return FunctionDeclBits.HasSkippedBody; }
26732677
void setHasSkippedBody(bool Skipped = true) {

clang/lib/AST/Decl.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3600,6 +3600,10 @@ bool FunctionDecl::isNoReturn() const {
36003600
return false;
36013601
}
36023602

3603+
bool FunctionDecl::isAnalyzerNoReturn() const {
3604+
return hasAttr<AnalyzerNoReturnAttr>();
3605+
}
3606+
36033607
bool FunctionDecl::isMemberLikeConstrainedFriend() const {
36043608
// C++20 [temp.friend]p9:
36053609
// A non-template friend declaration with a requires-clause [or]

clang/lib/Analysis/CFG.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2833,7 +2833,8 @@ CFGBlock *CFGBuilder::VisitCallExpr(CallExpr *C, AddStmtChoice asc) {
28332833
if (!FD->isVariadic())
28342834
findConstructionContextsForArguments(C);
28352835

2836-
if (FD->isNoReturn() || C->isBuiltinAssumeFalse(*Context))
2836+
if (FD->isNoReturn() || FD->isAnalyzerNoReturn() ||
2837+
C->isBuiltinAssumeFalse(*Context))
28372838
NoReturn = true;
28382839
if (FD->hasAttr<NoThrowAttr>())
28392840
AddEHEdge = false;

clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,80 @@ TEST_F(NoreturnDestructorTest, ConditionalOperatorNestedBranchReturns) {
693693
// FIXME: Called functions at point `p` should contain only "foo".
694694
}
695695

696+
class AnalyzerNoreturnTest : public Test {
697+
protected:
698+
template <typename Matcher>
699+
void runDataflow(llvm::StringRef Code, Matcher Expectations) {
700+
tooling::FileContentMappings FilesContents;
701+
FilesContents.push_back(
702+
std::make_pair<std::string, std::string>("noreturn_test_defs.h", R"(
703+
void assertionHandler() __attribute__((analyzer_noreturn));
704+
705+
void trap() {}
706+
)"));
707+
708+
ASSERT_THAT_ERROR(
709+
test::checkDataflow<FunctionCallAnalysis>(
710+
AnalysisInputs<FunctionCallAnalysis>(
711+
Code, ast_matchers::hasName("target"),
712+
[](ASTContext &C, Environment &) {
713+
return FunctionCallAnalysis(C);
714+
})
715+
.withASTBuildArgs({"-fsyntax-only", "-std=c++17"})
716+
.withASTBuildVirtualMappedFiles(std::move(FilesContents)),
717+
/*VerifyResults=*/
718+
[&Expectations](
719+
const llvm::StringMap<
720+
DataflowAnalysisState<FunctionCallLattice>> &Results,
721+
const AnalysisOutputs &) {
722+
EXPECT_THAT(Results, Expectations);
723+
}),
724+
llvm::Succeeded());
725+
}
726+
};
727+
728+
TEST_F(AnalyzerNoreturnTest, Breathing) {
729+
std::string Code = R"(
730+
#include "noreturn_test_defs.h"
731+
732+
void target() {
733+
trap();
734+
// [[p]]
735+
}
736+
)";
737+
runDataflow(Code, UnorderedElementsAre(IsStringMapEntry(
738+
"p", HoldsFunctionCallLattice(HasCalledFunctions(
739+
UnorderedElementsAre("trap"))))));
740+
}
741+
742+
TEST_F(AnalyzerNoreturnTest, DirectNoReturnCall) {
743+
std::string Code = R"(
744+
#include "noreturn_test_defs.h"
745+
746+
void target() {
747+
assertionHandler();
748+
trap();
749+
// [[p]]
750+
}
751+
)";
752+
runDataflow(Code, IsEmpty());
753+
}
754+
755+
TEST_F(AnalyzerNoreturnTest, CanonicalDeclCallCheck) {
756+
std::string Code = R"(
757+
#include "noreturn_test_defs.h"
758+
759+
extern void assertionHandler();
760+
761+
void target() {
762+
assertionHandler();
763+
trap();
764+
// [[p]]
765+
}
766+
)";
767+
runDataflow(Code, IsEmpty());
768+
}
769+
696770
// Models an analysis that uses flow conditions.
697771
class SpecialBoolAnalysis final
698772
: public DataflowAnalysis<SpecialBoolAnalysis, NoopLattice> {

0 commit comments

Comments
 (0)