[clang-tools-extra] [clang-tidy] Add frames for bugprone-exception-escape options (PR #187971)
Zeyi Xu via cfe-commits
cfe-commits at lists.llvm.org
Sun Apr 5 04:19:11 PDT 2026
https://github.com/zeyi2 updated https://github.com/llvm/llvm-project/pull/187971
>From c2add4b0e75cacf95491dcd52078f38c62daec98 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Mon, 23 Mar 2026 12:45:53 +0800
Subject: [PATCH 1/4] [clang-tidy] Add frames for bugprone-exception-escape
options
---
.../bugprone/ExceptionEscapeCheck.cpp | 39 ++++++++++++-------
.../clang-tidy/utils/ExceptionAnalyzer.cpp | 14 +++++--
.../clang-tidy/utils/ExceptionAnalyzer.h | 14 +++++++
...ions-without-specification-as-throwing.cpp | 31 ++++++++++++++-
4 files changed, 80 insertions(+), 18 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
index 41e0cdaf6ee61..07b7eb9a8ba94 100644
--- a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
@@ -146,24 +146,35 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
"%0 which should not throw exceptions")
<< MatchedDecl;
- if (Info.getExceptions().empty())
+ const utils::ExceptionAnalyzer::ExceptionInfo::Throwables &Exceptions =
+ Info.getExceptions();
+ const utils::ExceptionAnalyzer::ExceptionInfo::ThrowInfo *TI = nullptr;
+ if (!Exceptions.empty())
+ TI = &Exceptions.begin()->second;
+ else if (Info.containsUnknownElements())
+ TI = &Info.getUnknownThrowInfo();
+
+ if (!TI || TI->Loc.isInvalid())
return;
- const auto &[ThrowType, ThrowInfo] = *Info.getExceptions().begin();
-
- if (ThrowInfo.Loc.isInvalid())
- return;
-
- const utils::ExceptionAnalyzer::CallStack &Stack = ThrowInfo.Stack;
- diag(ThrowInfo.Loc,
- "frame #0: unhandled exception of type %0 may be thrown in function %1 "
- "here",
- DiagnosticIDs::Note)
- << QualType(ThrowType, 0U) << Stack.back().first;
+ if (!Exceptions.empty()) {
+ const auto &[ThrowType, ThrowInfo] = *Exceptions.begin();
+ diag(ThrowInfo.Loc,
+ "frame #0: unhandled exception of type %0 may be thrown in function "
+ "%1 here",
+ DiagnosticIDs::Note)
+ << QualType(ThrowType, 0U) << ThrowInfo.Stack.back().first;
+ } else {
+ diag(TI->Loc,
+ "frame #0: an exception of unknown type may be thrown in function %0 "
+ "here",
+ DiagnosticIDs::Note)
+ << TI->Stack.back().first;
+ }
size_t FrameNo = 1;
- for (auto CurrIt = ++Stack.rbegin(), PrevIt = Stack.rbegin();
- CurrIt != Stack.rend(); ++CurrIt, ++PrevIt) {
+ for (auto CurrIt = ++TI->Stack.rbegin(), PrevIt = TI->Stack.rbegin();
+ CurrIt != TI->Stack.rend(); ++CurrIt, ++PrevIt) {
const FunctionDecl *CurrFunction = CurrIt->first;
const FunctionDecl *PrevFunction = PrevIt->first;
const SourceLocation PrevLocation = PrevIt->second;
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index 60dade82e6155..9bffa5d1cf441 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -39,6 +39,8 @@ ExceptionAnalyzer::ExceptionInfo &ExceptionAnalyzer::ExceptionInfo::merge(
Behaviour = State::Unknown;
ContainsUnknown = ContainsUnknown || Other.ContainsUnknown;
+ if (!ThrowsUnknown && Other.ThrowsUnknown)
+ UnknownThrowInfo = Other.UnknownThrowInfo;
ThrowsUnknown = ThrowsUnknown || Other.ThrowsUnknown;
ThrownExceptions.insert_range(Other.ThrownExceptions);
return *this;
@@ -452,6 +454,7 @@ void ExceptionAnalyzer::ExceptionInfo::clear() {
Behaviour = State::NotThrowing;
ContainsUnknown = false;
ThrowsUnknown = false;
+ UnknownThrowInfo = {};
ThrownExceptions.clear();
}
@@ -489,7 +492,9 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
// are not explicitly non-throwing and no throw was discovered.
if (AssumeUnannotatedFunctionsAsThrowing &&
Result.getBehaviour() == State::NotThrowing && canThrow(Func)) {
- Result.registerUnknownException();
+ CallStack.insert({Func, CallLoc});
+ Result.registerUnknownException({Func->getLocation(), CallStack});
+ CallStack.erase(Func);
}
return Result;
}
@@ -507,8 +512,11 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
}
if (AssumeMissingDefinitionsFunctionsAsThrowing &&
- Result.getBehaviour() == State::Unknown)
- Result.registerUnknownException();
+ Result.getBehaviour() == State::Unknown) {
+ CallStack.insert({Func, CallLoc});
+ Result.registerUnknownException({Func->getLocation(), CallStack});
+ CallStack.erase(Func);
+ }
return Result;
}
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
index 08479ef58240a..c433cdb5d5193 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
@@ -117,6 +117,16 @@ class ExceptionAnalyzer {
ContainsUnknown = true;
}
+ /// Mark the entity as throwing due to an unknown cause and record Info
+ /// as the location for diagnostic notes.
+ void registerUnknownException(ThrowInfo Info) {
+ registerUnknownException();
+ UnknownThrowInfo = std::move(Info);
+ }
+
+ /// Return the location info recorded for the unknown throw, if any.
+ const ThrowInfo &getUnknownThrowInfo() const { return UnknownThrowInfo; }
+
private:
/// Recalculate the 'Behaviour' for example after filtering.
void reevaluateBehaviour();
@@ -134,6 +144,10 @@ class ExceptionAnalyzer {
/// based on analyzer configuration.
bool ThrowsUnknown = false;
+ /// Location info for the assumed-throwing function when ThrowsUnknown is
+ /// true. May be invalid if no location is available.
+ ThrowInfo UnknownThrowInfo;
+
/// 'ThrownException' is empty if the 'Behaviour' is either 'NotThrowing' or
/// 'Unknown'.
Throwables ThrownExceptions;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
index 6e9aa03323ec7..d9d6b2d922511 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
@@ -15,6 +15,8 @@ void unannotated_no_throw_body() {}
void calls_unannotated() noexcept {
// CHECK-MESSAGES-ALL: :[[@LINE-1]]:6: warning: an exception may be thrown in function 'calls_unannotated' which should not throw exceptions
+ // CHECK-MESSAGES-ALL: :[[@LINE-4]]:6: note: frame #0: an exception of unknown type may be thrown in function 'unannotated_no_throw_body' here
+ // CHECK-MESSAGES-ALL: :[[@LINE+3]]:3: note: frame #1: function 'calls_unannotated' calls function 'unannotated_no_throw_body' here
// CHECK-MESSAGES-UNDEFINED-NOT: warning:
// CHECK-MESSAGES-NONE-NOT: warning:
unannotated_no_throw_body();
@@ -24,7 +26,11 @@ void extern_declared();
void calls_unknown() noexcept {
// CHECK-MESSAGES-ALL: :[[@LINE-1]]:6: warning: an exception may be thrown in function 'calls_unknown' which should not throw exceptions
- // CHECK-MESSAGES-UNDEFINED: :[[@LINE-2]]:6: warning: an exception may be thrown in function 'calls_unknown' which should not throw exceptions
+ // CHECK-MESSAGES-ALL: :[[@LINE-4]]:6: note: frame #0: an exception of unknown type may be thrown in function 'extern_declared' here
+ // CHECK-MESSAGES-ALL: :[[@LINE+5]]:3: note: frame #1: function 'calls_unknown' calls function 'extern_declared' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-4]]:6: warning: an exception may be thrown in function 'calls_unknown' which should not throw exceptions
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-7]]:6: note: frame #0: an exception of unknown type may be thrown in function 'extern_declared' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE+2]]:3: note: frame #1: function 'calls_unknown' calls function 'extern_declared' here
// CHECK-MESSAGES-NONE-NOT: warning:
extern_declared();
}
@@ -54,6 +60,29 @@ void call() noexcept {
nothrow_nobody();
}
+struct Member {
+ Member() noexcept {}
+ Member(const Member &) noexcept {}
+ Member &operator=(const Member &) noexcept { return *this; }
+ ~Member() noexcept {}
+};
+
+struct S {
+ // CHECK-MESSAGES-ALL: :[[@LINE-1]]:8: warning: an exception may be thrown in function 'S' which should not throw exceptions
+ // CHECK-MESSAGES-ALL: :[[@LINE-2]]:8: note: frame #0: an exception of unknown type may be thrown in function 'S' here
+ // CHECK-MESSAGES-ALL: :[[@LINE-3]]:8: warning: an exception may be thrown in function 'operator=' which should not throw exceptions
+ // CHECK-MESSAGES-ALL: :[[@LINE-4]]:8: note: frame #0: an exception of unknown type may be thrown in function 'operator=' here
+ // CHECK-MESSAGES-ALL: :[[@LINE-5]]:8: warning: an exception may be thrown in function '~S' which should not throw exceptions
+ // CHECK-MESSAGES-ALL: :[[@LINE-6]]:8: note: frame #0: an exception of unknown type may be thrown in function '~S' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-7]]:8: warning: an exception may be thrown in function 'S' which should not throw exceptions
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-8]]:8: note: frame #0: an exception of unknown type may be thrown in function 'S' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-9]]:8: warning: an exception may be thrown in function 'operator=' which should not throw exceptions
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-10]]:8: note: frame #0: an exception of unknown type may be thrown in function 'operator=' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-11]]:8: warning: an exception may be thrown in function '~S' which should not throw exceptions
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-12]]:8: note: frame #0: an exception of unknown type may be thrown in function '~S' here
+ Member m;
+};
+
void explicit_throw() { throw 1; }
void calls_explicit_throw() noexcept {
// CHECK-MESSAGES-ALL: :[[@LINE-1]]:6: warning: an exception may be thrown in function 'calls_explicit_throw' which should not throw exceptions
>From cd6ae99635bc537f6cac2b5220468d1af1021882 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Tue, 24 Mar 2026 11:33:46 +0800
Subject: [PATCH 2/4] better?
---
clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index 9bffa5d1cf441..306fb3a82d0be 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -487,15 +487,14 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
}
}
- CallStack.erase(Func);
// Optionally treat unannotated functions as potentially throwing if they
// are not explicitly non-throwing and no throw was discovered.
if (AssumeUnannotatedFunctionsAsThrowing &&
Result.getBehaviour() == State::NotThrowing && canThrow(Func)) {
- CallStack.insert({Func, CallLoc});
Result.registerUnknownException({Func->getLocation(), CallStack});
- CallStack.erase(Func);
}
+
+ CallStack.erase(Func);
return Result;
}
>From 69f8d9b87c8c32772c9549387058697f4395437f Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Sat, 28 Mar 2026 19:19:14 +0800
Subject: [PATCH 3/4] add fixme
---
...-escape-treat-functions-without-specification-as-throwing.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
index d9d6b2d922511..6f955fa5a012a 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
@@ -80,6 +80,7 @@ struct S {
// CHECK-MESSAGES-UNDEFINED: :[[@LINE-10]]:8: note: frame #0: an exception of unknown type may be thrown in function 'operator=' here
// CHECK-MESSAGES-UNDEFINED: :[[@LINE-11]]:8: warning: an exception may be thrown in function '~S' which should not throw exceptions
// CHECK-MESSAGES-UNDEFINED: :[[@LINE-12]]:8: note: frame #0: an exception of unknown type may be thrown in function '~S' here
+ // FIXME: clearly non-throwing functions should not be marked as throwing
Member m;
};
>From fa9757b08d1928002ed39ff35ce3b0fce21d39a8 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Sun, 5 Apr 2026 19:18:40 +0800
Subject: [PATCH 4/4] ~
---
.../bugprone/ExceptionEscapeCheck.cpp | 29 +++++++++----------
.../clang-tidy/utils/ExceptionAnalyzer.cpp | 11 +++----
.../clang-tidy/utils/ExceptionAnalyzer.h | 14 ---------
3 files changed, 17 insertions(+), 37 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
index 07b7eb9a8ba94..96a11c0d1bdd8 100644
--- a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
@@ -146,35 +146,32 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
"%0 which should not throw exceptions")
<< MatchedDecl;
- const utils::ExceptionAnalyzer::ExceptionInfo::Throwables &Exceptions =
- Info.getExceptions();
- const utils::ExceptionAnalyzer::ExceptionInfo::ThrowInfo *TI = nullptr;
- if (!Exceptions.empty())
- TI = &Exceptions.begin()->second;
- else if (Info.containsUnknownElements())
- TI = &Info.getUnknownThrowInfo();
-
- if (!TI || TI->Loc.isInvalid())
+ if (Info.getExceptions().empty())
return;
- if (!Exceptions.empty()) {
- const auto &[ThrowType, ThrowInfo] = *Exceptions.begin();
+ const auto &[ThrowType, ThrowInfo] = *Info.getExceptions().begin();
+
+ if (ThrowInfo.Loc.isInvalid())
+ return;
+
+ const utils::ExceptionAnalyzer::CallStack &Stack = ThrowInfo.Stack;
+ if (ThrowType) {
diag(ThrowInfo.Loc,
"frame #0: unhandled exception of type %0 may be thrown in function "
"%1 here",
DiagnosticIDs::Note)
- << QualType(ThrowType, 0U) << ThrowInfo.Stack.back().first;
+ << QualType(ThrowType, 0U) << Stack.back().first;
} else {
- diag(TI->Loc,
+ diag(ThrowInfo.Loc,
"frame #0: an exception of unknown type may be thrown in function %0 "
"here",
DiagnosticIDs::Note)
- << TI->Stack.back().first;
+ << Stack.back().first;
}
size_t FrameNo = 1;
- for (auto CurrIt = ++TI->Stack.rbegin(), PrevIt = TI->Stack.rbegin();
- CurrIt != TI->Stack.rend(); ++CurrIt, ++PrevIt) {
+ for (auto CurrIt = ++Stack.rbegin(), PrevIt = Stack.rbegin();
+ CurrIt != Stack.rend(); ++CurrIt, ++PrevIt) {
const FunctionDecl *CurrFunction = CurrIt->first;
const FunctionDecl *PrevFunction = PrevIt->first;
const SourceLocation PrevLocation = PrevIt->second;
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index 306fb3a82d0be..203eaa90419d8 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -12,7 +12,6 @@ namespace clang::tidy::utils {
void ExceptionAnalyzer::ExceptionInfo::registerException(
const Type *ExceptionType, const ThrowInfo &ThrowInfo) {
- assert(ExceptionType != nullptr && "Only valid types are accepted");
Behaviour = State::Throwing;
ThrownExceptions.insert({ExceptionType, ThrowInfo});
}
@@ -39,9 +38,6 @@ ExceptionAnalyzer::ExceptionInfo &ExceptionAnalyzer::ExceptionInfo::merge(
Behaviour = State::Unknown;
ContainsUnknown = ContainsUnknown || Other.ContainsUnknown;
- if (!ThrowsUnknown && Other.ThrowsUnknown)
- UnknownThrowInfo = Other.UnknownThrowInfo;
- ThrowsUnknown = ThrowsUnknown || Other.ThrowsUnknown;
ThrownExceptions.insert_range(Other.ThrownExceptions);
return *this;
}
@@ -434,6 +430,8 @@ ExceptionAnalyzer::ExceptionInfo::filterIgnoredExceptions(
// Therefore this slightly hacky implementation is required.
for (const auto &ThrownException : ThrownExceptions) {
const Type *T = ThrownException.getFirst();
+ if (!T)
+ continue;
if (const auto *TD = T->getAsTagDecl()) {
if (TD->getDeclName().isIdentifier()) {
if ((IgnoreBadAlloc &&
@@ -454,7 +452,6 @@ void ExceptionAnalyzer::ExceptionInfo::clear() {
Behaviour = State::NotThrowing;
ContainsUnknown = false;
ThrowsUnknown = false;
- UnknownThrowInfo = {};
ThrownExceptions.clear();
}
@@ -491,7 +488,7 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
// are not explicitly non-throwing and no throw was discovered.
if (AssumeUnannotatedFunctionsAsThrowing &&
Result.getBehaviour() == State::NotThrowing && canThrow(Func)) {
- Result.registerUnknownException({Func->getLocation(), CallStack});
+ Result.registerException(nullptr, {Func->getLocation(), CallStack});
}
CallStack.erase(Func);
@@ -513,7 +510,7 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
if (AssumeMissingDefinitionsFunctionsAsThrowing &&
Result.getBehaviour() == State::Unknown) {
CallStack.insert({Func, CallLoc});
- Result.registerUnknownException({Func->getLocation(), CallStack});
+ Result.registerException(nullptr, {Func->getLocation(), CallStack});
CallStack.erase(Func);
}
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
index c433cdb5d5193..08479ef58240a 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
@@ -117,16 +117,6 @@ class ExceptionAnalyzer {
ContainsUnknown = true;
}
- /// Mark the entity as throwing due to an unknown cause and record Info
- /// as the location for diagnostic notes.
- void registerUnknownException(ThrowInfo Info) {
- registerUnknownException();
- UnknownThrowInfo = std::move(Info);
- }
-
- /// Return the location info recorded for the unknown throw, if any.
- const ThrowInfo &getUnknownThrowInfo() const { return UnknownThrowInfo; }
-
private:
/// Recalculate the 'Behaviour' for example after filtering.
void reevaluateBehaviour();
@@ -144,10 +134,6 @@ class ExceptionAnalyzer {
/// based on analyzer configuration.
bool ThrowsUnknown = false;
- /// Location info for the assumed-throwing function when ThrowsUnknown is
- /// true. May be invalid if no location is available.
- ThrowInfo UnknownThrowInfo;
-
/// 'ThrownException' is empty if the 'Behaviour' is either 'NotThrowing' or
/// 'Unknown'.
Throwables ThrownExceptions;
More information about the cfe-commits
mailing list