[clang-tools-extra] 20bf8e0 - [clang-tidy] Add options to throw unannotated functions in `bugprone-exception-escape` (#168324)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Feb 9 18:57:16 PST 2026
Author: mitchell
Date: 2026-02-10T10:57:11+08:00
New Revision: 20bf8e0684836c48f3a5f58cafd630239c1c9385
URL: https://github.com/llvm/llvm-project/commit/20bf8e0684836c48f3a5f58cafd630239c1c9385
DIFF: https://github.com/llvm/llvm-project/commit/20bf8e0684836c48f3a5f58cafd630239c1c9385.diff
LOG: [clang-tidy] Add options to throw unannotated functions in `bugprone-exception-escape` (#168324)
As of AI Usage: Gemini 3 was used for rephrasing the documentation.
Closes https://github.com/llvm/llvm-project/issues/164795
---------
Co-authored-by: EugeneZelenko <eugene.zelenko at gmail.com>
Co-authored-by: Baranov Victor <bar.victor.2002 at gmail.com>
Added:
clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
Modified:
clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h
clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
clang-tools-extra/docs/ReleaseNotes.rst
clang-tools-extra/docs/clang-tidy/checks/bugprone/exception-escape.rst
Removed:
################################################################################
diff --git a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
index 1cfb1511fa94e..41e0cdaf6ee61 100644
--- a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
@@ -13,7 +13,29 @@
using namespace clang::ast_matchers;
-namespace clang::tidy::bugprone {
+namespace clang::tidy {
+
+template <>
+struct OptionEnumMapping<
+ bugprone::ExceptionEscapeCheck::TreatFunctionsWithoutSpecification> {
+ using TreatFunctionsWithoutSpecification =
+ bugprone::ExceptionEscapeCheck::TreatFunctionsWithoutSpecification;
+
+ static llvm::ArrayRef<
+ std::pair<TreatFunctionsWithoutSpecification, StringRef>>
+ getEnumMapping() {
+ static constexpr std::pair<TreatFunctionsWithoutSpecification, StringRef>
+ Mapping[] = {
+ {TreatFunctionsWithoutSpecification::None, "None"},
+ {TreatFunctionsWithoutSpecification::OnlyUndefined,
+ "OnlyUndefined"},
+ {TreatFunctionsWithoutSpecification::All, "All"},
+ };
+ return {Mapping};
+ }
+};
+
+namespace bugprone {
namespace {
AST_MATCHER_P(FunctionDecl, isEnabled, llvm::StringSet<>,
@@ -42,7 +64,10 @@ ExceptionEscapeCheck::ExceptionEscapeCheck(StringRef Name,
CheckDestructors(Options.get("CheckDestructors", true)),
CheckMoveMemberFunctions(Options.get("CheckMoveMemberFunctions", true)),
CheckMain(Options.get("CheckMain", true)),
- CheckNothrowFunctions(Options.get("CheckNothrowFunctions", true)) {
+ CheckNothrowFunctions(Options.get("CheckNothrowFunctions", true)),
+ TreatFunctionsWithoutSpecificationAsThrowing(
+ Options.get("TreatFunctionsWithoutSpecificationAsThrowing",
+ TreatFunctionsWithoutSpecification::None)) {
llvm::SmallVector<StringRef, 8> FunctionsThatShouldNotThrowVec,
IgnoredExceptionsVec, CheckedSwapFunctionsVec;
RawFunctionsThatShouldNotThrow.split(FunctionsThatShouldNotThrowVec, ",", -1,
@@ -57,6 +82,14 @@ ExceptionEscapeCheck::ExceptionEscapeCheck(StringRef Name,
IgnoredExceptions.insert_range(IgnoredExceptionsVec);
Tracer.ignoreExceptions(std::move(IgnoredExceptions));
Tracer.ignoreBadAlloc(true);
+
+ Tracer.assumeMissingDefinitionsFunctionsAsThrowing(
+ TreatFunctionsWithoutSpecificationAsThrowing !=
+ TreatFunctionsWithoutSpecification::None);
+
+ Tracer.assumeUnannotatedFunctionsAsThrowing(
+ TreatFunctionsWithoutSpecificationAsThrowing ==
+ TreatFunctionsWithoutSpecification::All);
}
void ExceptionEscapeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
@@ -68,6 +101,8 @@ void ExceptionEscapeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, "CheckMoveMemberFunctions", CheckMoveMemberFunctions);
Options.store(Opts, "CheckMain", CheckMain);
Options.store(Opts, "CheckNothrowFunctions", CheckNothrowFunctions);
+ Options.store(Opts, "TreatFunctionsWithoutSpecificationAsThrowing",
+ TreatFunctionsWithoutSpecificationAsThrowing);
}
void ExceptionEscapeCheck::registerMatchers(MatchFinder *Finder) {
@@ -111,6 +146,9 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
"%0 which should not throw exceptions")
<< MatchedDecl;
+ if (Info.getExceptions().empty())
+ return;
+
const auto &[ThrowType, ThrowInfo] = *Info.getExceptions().begin();
if (ThrowInfo.Loc.isInvalid())
@@ -142,4 +180,5 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
}
}
-} // namespace clang::tidy::bugprone
+} // namespace bugprone
+} // namespace clang::tidy
diff --git a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h
index c3bf4a4335273..7c970aa57e2ef 100644
--- a/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h
@@ -32,6 +32,12 @@ class ExceptionEscapeCheck : public ClangTidyCheck {
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ enum class TreatFunctionsWithoutSpecification {
+ None,
+ OnlyUndefined,
+ All,
+ };
+
private:
StringRef RawFunctionsThatShouldNotThrow;
StringRef RawIgnoredExceptions;
@@ -42,6 +48,9 @@ class ExceptionEscapeCheck : public ClangTidyCheck {
const bool CheckMain;
const bool CheckNothrowFunctions;
+ const TreatFunctionsWithoutSpecification
+ TreatFunctionsWithoutSpecificationAsThrowing;
+
llvm::StringSet<> FunctionsThatShouldNotThrow;
llvm::StringSet<> CheckedSwapFunctions;
utils::ExceptionAnalyzer Tracer;
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index f766a1bca655c..3f8bca6ac4d87 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -39,6 +39,7 @@ ExceptionAnalyzer::ExceptionInfo &ExceptionAnalyzer::ExceptionInfo::merge(
Behaviour = State::Unknown;
ContainsUnknown = ContainsUnknown || Other.ContainsUnknown;
+ ThrowsUnknown = ThrowsUnknown || Other.ThrowsUnknown;
ThrownExceptions.insert_range(Other.ThrownExceptions);
return *this;
}
@@ -450,11 +451,12 @@ ExceptionAnalyzer::ExceptionInfo::filterIgnoredExceptions(
void ExceptionAnalyzer::ExceptionInfo::clear() {
Behaviour = State::NotThrowing;
ContainsUnknown = false;
+ ThrowsUnknown = false;
ThrownExceptions.clear();
}
void ExceptionAnalyzer::ExceptionInfo::reevaluateBehaviour() {
- if (ThrownExceptions.empty())
+ if (ThrownExceptions.empty() && !ThrowsUnknown)
if (ContainsUnknown)
Behaviour = State::Unknown;
else
@@ -483,10 +485,17 @@ 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)) {
+ Result.registerUnknownException();
+ }
return Result;
}
auto Result = ExceptionInfo::createUnknown();
+
if (const auto *FPT = Func->getType()->getAs<FunctionProtoType>()) {
for (const QualType &Ex : FPT->exceptions()) {
CallStack.insert({Func, CallLoc});
@@ -496,6 +505,11 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
CallStack.erase(Func);
}
}
+
+ if (AssumeMissingDefinitionsFunctionsAsThrowing &&
+ Result.getBehaviour() == State::Unknown)
+ Result.registerUnknownException();
+
return Result;
}
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
index 1a277c8a6d3b2..08479ef58240a 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
@@ -52,7 +52,7 @@ class ExceptionAnalyzer {
using Throwables = llvm::SmallDenseMap<const Type *, ThrowInfo, 2>;
static ExceptionInfo createUnknown() { return {State::Unknown}; }
- static ExceptionInfo createNonThrowing() { return {State::Throwing}; }
+ static ExceptionInfo createNonThrowing() { return {State::NotThrowing}; }
/// By default the exception situation is unknown and must be
/// clarified step-wise.
@@ -111,6 +111,12 @@ class ExceptionAnalyzer {
/// occur. If there is an 'Unknown' element this can not be guaranteed.
bool containsUnknownElements() const { return ContainsUnknown; }
+ void registerUnknownException() {
+ Behaviour = State::Throwing;
+ ThrowsUnknown = true;
+ ContainsUnknown = true;
+ }
+
private:
/// Recalculate the 'Behaviour' for example after filtering.
void reevaluateBehaviour();
@@ -124,6 +130,10 @@ class ExceptionAnalyzer {
/// after filtering.
bool ContainsUnknown;
+ /// True if the entity is determined to be Throwing due to an unknown cause,
+ /// based on analyzer configuration.
+ bool ThrowsUnknown = false;
+
/// 'ThrownException' is empty if the 'Behaviour' is either 'NotThrowing' or
/// 'Unknown'.
Throwables ThrownExceptions;
@@ -131,6 +141,15 @@ class ExceptionAnalyzer {
ExceptionAnalyzer() = default;
+ void assumeUnannotatedFunctionsAsThrowing(bool AssumeUnannotatedAsThrowing) {
+ AssumeUnannotatedFunctionsAsThrowing = AssumeUnannotatedAsThrowing;
+ }
+ void assumeMissingDefinitionsFunctionsAsThrowing(
+ bool AssumeMissingDefinitionsAsThrowing) {
+ AssumeMissingDefinitionsFunctionsAsThrowing =
+ AssumeMissingDefinitionsAsThrowing;
+ }
+
void ignoreBadAlloc(bool ShallIgnore) { IgnoreBadAlloc = ShallIgnore; }
void ignoreExceptions(llvm::StringSet<> ExceptionNames) {
IgnoredExceptions = std::move(ExceptionNames);
@@ -154,6 +173,8 @@ class ExceptionAnalyzer {
bool IgnoreBadAlloc = true;
llvm::StringSet<> IgnoredExceptions;
llvm::DenseMap<const FunctionDecl *, ExceptionInfo> FunctionCache{32U};
+ bool AssumeUnannotatedFunctionsAsThrowing = false;
+ bool AssumeMissingDefinitionsFunctionsAsThrowing = false;
};
} // namespace clang::tidy::utils
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 7b5b332bdb8a2..539d2e208a162 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -144,6 +144,12 @@ Changes in existing checks
<clang-tidy/checks/bugprone/argument-comment>` to also check for C++11
inherited constructors.
+- Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check by adding
+ `TreatFunctionsWithoutSpecificationAsThrowing` option to support reporting
+ for unannotated functions, enabling reporting when no explicit ``throw``
+ is seen and allowing separate tuning for known and unknown implementations.
+
- Improved :doc:`bugprone-macro-parentheses
<clang-tidy/checks/bugprone/macro-parentheses>` check by printing the macro
definition in the warning message if the macro is defined on command line.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/exception-escape.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/exception-escape.rst
index 7d724a4581715..a655661c57871 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/exception-escape.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/exception-escape.rst
@@ -71,3 +71,22 @@ Options
Comma separated list containing type names which are not counted as thrown
exceptions in the check. Default value is an empty string.
+
+.. option:: TreatFunctionsWithoutSpecificationAsThrowing
+
+ Determines which functions are considered as throwing if they do not have
+ an explicit exception specification. It can be set to the following values:
+
+ - `None`
+ The check will consider functions without an explicit exception
+ specification as throwing only if they have a visible definition which
+ can be deduced to throw.
+ - `OnlyUndefined`
+ The check will consider functions with only a declaration available and
+ no visible definition as throwing.
+ - `All`
+ The check will consider all functions without an explicit exception
+ specification (such as ``noexcept``) as throwing, even if they have a
+ visible definition and do not contain any throwing statements.
+
+ Default value is `None`.
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
new file mode 100644
index 0000000000000..6e9aa03323ec7
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape-treat-functions-without-specification-as-throwing.cpp
@@ -0,0 +1,69 @@
+// RUN: %check_clang_tidy -check-suffixes=ALL -std=c++11-or-later %s bugprone-exception-escape %t -- \
+// RUN: -config='{"CheckOptions": { \
+// RUN: "bugprone-exception-escape.TreatFunctionsWithoutSpecificationAsThrowing": "All" \
+// RUN: }}' -- -fexceptions
+// RUN: %check_clang_tidy -check-suffixes=UNDEFINED -std=c++11-or-later %s bugprone-exception-escape %t -- \
+// RUN: -config='{"CheckOptions": { \
+// RUN: "bugprone-exception-escape.TreatFunctionsWithoutSpecificationAsThrowing": "OnlyUndefined" \
+// RUN: }}' -- -fexceptions
+// RUN: %check_clang_tidy -check-suffixes=NONE -std=c++11-or-later %s bugprone-exception-escape %t -- \
+// RUN: -config='{"CheckOptions": { \
+// RUN: "bugprone-exception-escape.TreatFunctionsWithoutSpecificationAsThrowing": "None" \
+// RUN: }}' -- -fexceptions
+
+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-UNDEFINED-NOT: warning:
+ // CHECK-MESSAGES-NONE-NOT: warning:
+ unannotated_no_throw_body();
+}
+
+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-NONE-NOT: warning:
+ extern_declared();
+}
+
+void calls_unknown_caught() noexcept {
+ // CHECK-MESSAGES-ALL-NOT: warning:
+ // CHECK-MESSAGES-UNDEFINED-NOT: warning:
+ // CHECK-MESSAGES-NONE-NOT: warning:
+ try {
+ extern_declared();
+ } catch(...) {
+ }
+}
+
+void definitely_nothrow() noexcept {}
+
+void calls_nothrow() noexcept {
+ // CHECK-MESSAGES-ALL-NOT: warning:
+ // CHECK-MESSAGES-UNDEFINED-NOT: warning:
+ // CHECK-MESSAGES-NONE-NOT: warning:
+ definitely_nothrow();
+}
+
+void nothrow_nobody() throw();
+
+void call() noexcept {
+ nothrow_nobody();
+}
+
+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
+ // CHECK-MESSAGES-ALL: :[[@LINE-3]]:25: note: frame #0: unhandled exception of type 'int' may be thrown in function 'explicit_throw' here
+ // CHECK-MESSAGES-ALL: :[[@LINE+7]]:3: note: frame #1: function 'calls_explicit_throw' calls function 'explicit_throw' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-4]]:6: warning: an exception may be thrown in function 'calls_explicit_throw' which should not throw exceptions
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE-6]]:25: note: frame #0: unhandled exception of type 'int' may be thrown in function 'explicit_throw' here
+ // CHECK-MESSAGES-UNDEFINED: :[[@LINE+4]]:3: note: frame #1: function 'calls_explicit_throw' calls function 'explicit_throw' here
+ // CHECK-MESSAGES-NONE: :[[@LINE-7]]:6: warning: an exception may be thrown in function 'calls_explicit_throw' which should not throw exceptions
+ // CHECK-MESSAGES-NONE: :[[@LINE-9]]:25: note: frame #0: unhandled exception of type 'int' may be thrown in function 'explicit_throw' here
+ // CHECK-MESSAGES-NONE: :[[@LINE+1]]:3: note: frame #1: function 'calls_explicit_throw' calls function 'explicit_throw' here
+ explicit_throw();
+}
More information about the cfe-commits
mailing list