Skip to content

Conversation

localspook
Copy link
Contributor

Fixes #132605.

@llvmbot
Copy link
Member

llvmbot commented Sep 24, 2025

@llvm/pr-subscribers-clang-tidy

Author: Victor Chernyakin (localspook)

Changes

Fixes #132605.


Full diff: https://github.com/llvm/llvm-project/pull/160592.diff

3 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp (+5)
  • (modified) clang-tools-extra/docs/ReleaseNotes.rst (+5)
  • (modified) clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp (+38)
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index bdde7249d2796..fd4320eb8144b 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -595,6 +595,11 @@ ExceptionAnalyzer::throwsException(const Stmt *St,
         Results.merge(DestructorExcs);
       }
     }
+  } else if (const auto *Lambda = dyn_cast<LambdaExpr>(St)) {
+    for (const Stmt *Init : Lambda->capture_inits()) {
+      ExceptionInfo Excs = throwsException(Init, Caught, CallStack);
+      Results.merge(Excs);
+    }
   } else {
     for (const Stmt *Child : St->children()) {
       ExceptionInfo Excs = throwsException(Child, Caught, CallStack);
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 7cdff86beeec6..63ea45cb39270 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -210,6 +210,11 @@ Changes in existing checks
   correcting a spelling mistake on its option
   ``NamePrefixSuffixSilenceDissimilarityTreshold``.
 
+- Improved :doc:`bugprone-exception-escape
+  <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+  exceptions from captures are now diagnosed, exceptions in the bodies of
+  lambdas that aren't actually invoked are not.
+
 - Improved :doc:`bugprone-infinite-loop
   <clang-tidy/checks/bugprone/infinite-loop>` check by adding detection for
   variables introduced by structured bindings.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
index b10bd1d482867..da200cd6de8df 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
@@ -894,3 +894,41 @@ void pointer_exception_can_not_escape_with_void_handler() noexcept {
   } catch (void *) {
   }
 }
+
+void throw_in_uninvoked_lambda() noexcept {
+  [] { throw 42; };
+}
+
+struct copy_constructor_throws {
+  copy_constructor_throws(const copy_constructor_throws&) { throw 42; }
+};
+
+void throw_in_lambda_default_by_value_capture(const copy_constructor_throws& a) noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda_default_by_value_capture' which should not throw exceptions
+  [=] { a; };
+  // CHECK-MESSAGES: :[[@LINE-6]]:61: note: frame #0: unhandled exception of type 'int' may be thrown in function 'copy_constructor_throws' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:4: note: frame #1: function 'throw_in_lambda_default_by_value_capture' calls function 'copy_constructor_throws' here
+}
+
+void throw_in_lambda_explicit_by_value_capture(const copy_constructor_throws& a) noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda_explicit_by_value_capture' which should not throw exceptions
+  [a] {};
+  // CHECK-MESSAGES: :[[@LINE-13]]:61: note: frame #0: unhandled exception of type 'int' may be thrown in function 'copy_constructor_throws' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:4: note: frame #1: function 'throw_in_lambda_explicit_by_value_capture' calls function 'copy_constructor_throws' here
+}
+
+void no_throw_in_lambda_by_reference_capture(const copy_constructor_throws& a) noexcept {
+  [&] { a; };
+  [&a] {};
+}
+
+void throw_in_uninvoked_lambda() noexcept {
+  [] { throw 42; };
+}
+
+void throw_in_lambda() noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda' which should not throw exceptions
+  [] { throw 42; }();
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: note: frame #0: unhandled exception of type 'int' may be thrown in function 'operator()' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:19: note: frame #1: function 'throw_in_lambda' calls function 'operator()' here
+}

@llvmbot
Copy link
Member

llvmbot commented Sep 24, 2025

@llvm/pr-subscribers-clang-tools-extra

Author: Victor Chernyakin (localspook)

Changes

Fixes #132605.


Full diff: https://github.com/llvm/llvm-project/pull/160592.diff

3 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp (+5)
  • (modified) clang-tools-extra/docs/ReleaseNotes.rst (+5)
  • (modified) clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp (+38)
diff --git a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
index bdde7249d2796..fd4320eb8144b 100644
--- a/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
+++ b/clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
@@ -595,6 +595,11 @@ ExceptionAnalyzer::throwsException(const Stmt *St,
         Results.merge(DestructorExcs);
       }
     }
+  } else if (const auto *Lambda = dyn_cast<LambdaExpr>(St)) {
+    for (const Stmt *Init : Lambda->capture_inits()) {
+      ExceptionInfo Excs = throwsException(Init, Caught, CallStack);
+      Results.merge(Excs);
+    }
   } else {
     for (const Stmt *Child : St->children()) {
       ExceptionInfo Excs = throwsException(Child, Caught, CallStack);
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 7cdff86beeec6..63ea45cb39270 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -210,6 +210,11 @@ Changes in existing checks
   correcting a spelling mistake on its option
   ``NamePrefixSuffixSilenceDissimilarityTreshold``.
 
+- Improved :doc:`bugprone-exception-escape
+  <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+  exceptions from captures are now diagnosed, exceptions in the bodies of
+  lambdas that aren't actually invoked are not.
+
 - Improved :doc:`bugprone-infinite-loop
   <clang-tidy/checks/bugprone/infinite-loop>` check by adding detection for
   variables introduced by structured bindings.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
index b10bd1d482867..da200cd6de8df 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/exception-escape.cpp
@@ -894,3 +894,41 @@ void pointer_exception_can_not_escape_with_void_handler() noexcept {
   } catch (void *) {
   }
 }
+
+void throw_in_uninvoked_lambda() noexcept {
+  [] { throw 42; };
+}
+
+struct copy_constructor_throws {
+  copy_constructor_throws(const copy_constructor_throws&) { throw 42; }
+};
+
+void throw_in_lambda_default_by_value_capture(const copy_constructor_throws& a) noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda_default_by_value_capture' which should not throw exceptions
+  [=] { a; };
+  // CHECK-MESSAGES: :[[@LINE-6]]:61: note: frame #0: unhandled exception of type 'int' may be thrown in function 'copy_constructor_throws' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:4: note: frame #1: function 'throw_in_lambda_default_by_value_capture' calls function 'copy_constructor_throws' here
+}
+
+void throw_in_lambda_explicit_by_value_capture(const copy_constructor_throws& a) noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda_explicit_by_value_capture' which should not throw exceptions
+  [a] {};
+  // CHECK-MESSAGES: :[[@LINE-13]]:61: note: frame #0: unhandled exception of type 'int' may be thrown in function 'copy_constructor_throws' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:4: note: frame #1: function 'throw_in_lambda_explicit_by_value_capture' calls function 'copy_constructor_throws' here
+}
+
+void no_throw_in_lambda_by_reference_capture(const copy_constructor_throws& a) noexcept {
+  [&] { a; };
+  [&a] {};
+}
+
+void throw_in_uninvoked_lambda() noexcept {
+  [] { throw 42; };
+}
+
+void throw_in_lambda() noexcept {
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: an exception may be thrown in function 'throw_in_lambda' which should not throw exceptions
+  [] { throw 42; }();
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: note: frame #0: unhandled exception of type 'int' may be thrown in function 'operator()' here
+  // CHECK-MESSAGES: :[[@LINE-2]]:19: note: frame #1: function 'throw_in_lambda' calls function 'operator()' here
+}

@vbvictor
Copy link
Contributor

I'd generally wish to add more edge cases for lambdas, just as with functions.
I guess we twist a little how lambdas are processed so we should cover it with more tests

@vbvictor
Copy link
Contributor

vbvictor commented Sep 24, 2025

Also, could you share how we traversed before the fix and now? (If you debugged it)
Given the AST for [] { throw 0; }; (stipped useless part):

    `-LambdaExpr <line:2:5, col:19> '(lambda at line:2:5)'
      |-CXXRecordDecl <col:5> col:5 implicit class definition
      | |-CXXMethodDecl <col:6, col:19> col:5 constexpr operator() 'auto () const -> void' inline
      | | |-CompoundStmt <col:8, col:19>
      | | | `-CXXThrowExpr <col:10, col:16> 'void'
      | | |   `-IntegerLiteral <col:16> 'int' 0
      | | `-InferredNoReturnAttr Implicit
      | |-CXXConversionDecl <col:5, col:19> col:5 implicit constexpr operator void (*)() 'auto (*() const noexcept)() -> void' inline
      | |-CXXMethodDecl <col:5, col:19> col:5 implicit __invoke 'auto () -> void' static inline
      | `-CXXDestructorDecl <col:5> col:5 implicit referenced ~(lambda at line:2:5) 'void () noexcept' inline default trivial
      `-CompoundStmt <col:8, col:19>
        `-CXXThrowExpr <col:10, col:16> 'void'
          `-IntegerLiteral <col:16> 'int' 0

I thought we needed some modelling for calling the lambda in order to properly handle [] {} but in your PR it's different.
Honestly from brief look I don't understand how this fix the problem (guess I'd have to come back a little later for second go)

@localspook
Copy link
Contributor Author

localspook commented Sep 24, 2025

Before this change, the code was going into this branch:

} else {
for (const Stmt *Child : St->children()) {
ExceptionInfo Excs = throwsException(Child, Caught, CallStack);
Results.merge(Excs);
}
}

That's the same branch it goes into when it processes a CompoundStmt. In other words, it confused this:

void f() noexcept { [] { throw 42; }; }

for this:

void f() noexcept { { throw 42; } }

My change only affects the lambda expression itself; the actual call to a lambda goes through the existing code paths.

Copy link
Contributor

@HerrCai0907 HerrCai0907 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

could you add some test of

  1. nested lambda
  2. more complex case in lambda e.g. call a function may throw.

@localspook
Copy link
Contributor Author

Added some more tests, tell me if I should add any others

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@localspook localspook merged commit 0e93ab8 into llvm:main Sep 26, 2025
11 checks passed
@localspook localspook deleted the exception-escape branch September 26, 2025 20:50
mahesh-attarde pushed a commit to mahesh-attarde/llvm-project that referenced this pull request Oct 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[clang-tidy] False positive bugprone-exception-escape for throwing lambda in noexcept function
4 participants