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 <[email protected]>
Co-authored-by: Baranov Victor <[email protected]>

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();
+}


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to