https://github.com/balazske created https://github.com/llvm/llvm-project/pull/176430
`ExceptionEscapeCheck` does not warn if a function is `noexcept(false)` (may throw) but is one of the functions where throwing exceptions is unsafe. This check can be used to find such cases. From 5f4e66967cc52652983cd387a544388c65bed727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20K=C3=A9ri?= <[email protected]> Date: Fri, 16 Jan 2026 17:31:35 +0100 Subject: [PATCH] [clang-tidy] Add check 'bugprone-unsafe-to-allow-exceptions' --- .../bugprone/BugproneTidyModule.cpp | 3 + .../clang-tidy/bugprone/CMakeLists.txt | 1 + .../bugprone/UnsafeToAllowExceptionsCheck.cpp | 76 +++++++++++++++++++ .../bugprone/UnsafeToAllowExceptionsCheck.h | 39 ++++++++++ .../bugprone/unsafe-to-allow-exceptions.rst | 31 ++++++++ .../docs/clang-tidy/checks/list.rst | 1 + .../bugprone/unsafe-to-allow-exceptions.cpp | 45 +++++++++++ 7 files changed, 196 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-to-allow-exceptions.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-to-allow-exceptions.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp index 4150442c25d61..310184037afbd 100644 --- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp @@ -106,6 +106,7 @@ #include "UnintendedCharOstreamOutputCheck.h" #include "UniquePtrArrayMismatchCheck.h" #include "UnsafeFunctionsCheck.h" +#include "UnsafeToAllowExceptionsCheck.h" #include "UnusedLocalNonTrivialVariableCheck.h" #include "UnusedRaiiCheck.h" #include "UnusedReturnValueCheck.h" @@ -308,6 +309,8 @@ class BugproneModule : public ClangTidyModule { "bugprone-crtp-constructor-accessibility"); CheckFactories.registerCheck<UnsafeFunctionsCheck>( "bugprone-unsafe-functions"); + CheckFactories.registerCheck<UnsafeToAllowExceptionsCheck>( + "bugprone-unsafe-to-allow-exceptions"); CheckFactories.registerCheck<UnusedLocalNonTrivialVariableCheck>( "bugprone-unused-local-non-trivial-variable"); CheckFactories.registerCheck<UnusedRaiiCheck>("bugprone-unused-raii"); diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt index db1256d91d311..96ad671d03b39 100644 --- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt @@ -108,6 +108,7 @@ add_clang_library(clangTidyBugproneModule STATIC UnhandledSelfAssignmentCheck.cpp UniquePtrArrayMismatchCheck.cpp UnsafeFunctionsCheck.cpp + UnsafeToAllowExceptionsCheck.cpp UnusedLocalNonTrivialVariableCheck.cpp UnusedRaiiCheck.cpp UnusedReturnValueCheck.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.cpp new file mode 100644 index 0000000000000..3f159410d6f4d --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.cpp @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "UnsafeToAllowExceptionsCheck.h" + +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "llvm/ADT/StringSet.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::bugprone { +namespace { + +AST_MATCHER_P(FunctionDecl, isEnabled, llvm::StringSet<>, + FunctionsThatShouldNotThrow) { + return FunctionsThatShouldNotThrow.contains(Node.getNameAsString()); +} + +AST_MATCHER(FunctionDecl, isExplicitThrow) { + return isExplicitThrowExceptionSpec(Node.getExceptionSpecType()) && + Node.getExceptionSpecSourceRange().isValid(); +} + +AST_MATCHER(FunctionDecl, hasAtLeastOneParameter) { + return Node.getNumParams() > 0; +} + +} // namespace + +UnsafeToAllowExceptionsCheck::UnsafeToAllowExceptionsCheck( + StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + RawCheckedSwapFunctions( + Options.get("CheckedSwapFunctions", "swap,iter_swap,iter_move")) { + llvm::SmallVector<StringRef, 4> CheckedSwapFunctionsVec; + RawCheckedSwapFunctions.split(CheckedSwapFunctionsVec, ",", -1, false); + CheckedSwapFunctions.insert_range(CheckedSwapFunctionsVec); +} + +void UnsafeToAllowExceptionsCheck::storeOptions( + ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "CheckedSwapFunctions", RawCheckedSwapFunctions); +} + +void UnsafeToAllowExceptionsCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher( + functionDecl(allOf(isDefinition(), isExplicitThrow(), + anyOf(cxxDestructorDecl(), + cxxConstructorDecl(isMoveConstructor()), + cxxMethodDecl(isMoveAssignmentOperator()), + allOf(isEnabled(CheckedSwapFunctions), + hasAtLeastOneParameter()), + isMain()))) + .bind("f"), + this); +} + +void UnsafeToAllowExceptionsCheck::check( + const MatchFinder::MatchResult &Result) { + const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("f"); + + if (!MatchedDecl) + return; + + diag(MatchedDecl->getLocation(), + "function %0 should not throw exceptions but " + "it is still marked as throwable") + << MatchedDecl; +} + +} // namespace clang::tidy::bugprone diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.h new file mode 100644 index 0000000000000..90b9d42a7e4c4 --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeToAllowExceptionsCheck.h @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFETOALLOWEXCEPTIONSCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFETOALLOWEXCEPTIONSCHECK_H + +#include "../ClangTidyCheck.h" +#include "llvm/ADT/StringSet.h" + +namespace clang::tidy::bugprone { + +/// Finds functions where throwing exceptions is unsafe but the function is +/// still marked as throwable. +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unsafe-to-allow-exceptions.html +class UnsafeToAllowExceptionsCheck : public ClangTidyCheck { +public: + UnsafeToAllowExceptionsCheck(StringRef Name, ClangTidyContext *Context); + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.CPlusPlus && LangOpts.CXXExceptions; + } + void storeOptions(ClangTidyOptions::OptionMap &Opts) override; + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + +private: + StringRef RawCheckedSwapFunctions; + llvm::StringSet<> CheckedSwapFunctions; +}; + +} // namespace clang::tidy::bugprone + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFETOALLOWEXCEPTIONSCHECK_H diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-to-allow-exceptions.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-to-allow-exceptions.rst new file mode 100644 index 0000000000000..56d6507f7f207 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-to-allow-exceptions.rst @@ -0,0 +1,31 @@ +.. title:: clang-tidy - bugprone-unsafe-to-allow-exceptions + +bugprone-unsafe-to-allow-exceptions +=================================== + +Finds functions where throwing exceptions is unsafe but the function is still +marked as throwable. Throwing exceptions from the following functions can be +problematic: + +* Destructors +* Move constructors +* Move assignment operators +* The ``main()`` functions +* ``swap()`` functions +* ``iter_swap()`` functions +* ``iter_move()`` functions + +The check finds any of these functions if it is marked with ``noexcept(false)`` +or ``throw(exception)``. This would indicate that the function is expected to +throw exceptions. Only the presence of these keywords is checked, not if the +function actually throws any exception. + +Options +------- + +.. option:: CheckedSwapFunctions + + Comma-separated list of checked swap function names (where throwing + exceptions is unsafe). + Default value is `swap,iter_swap,iter_move`. + diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 3dabf887dc2e1..5bef16e365524 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -176,6 +176,7 @@ Clang-Tidy Checks :doc:`bugprone-unintended-char-ostream-output <bugprone/unintended-char-ostream-output>`, "Yes" :doc:`bugprone-unique-ptr-array-mismatch <bugprone/unique-ptr-array-mismatch>`, "Yes" :doc:`bugprone-unsafe-functions <bugprone/unsafe-functions>`, + :doc:`bugprone-unsafe-to-allow-exceptions <bugprone/unsafe-to-allow-exceptions>`, :doc:`bugprone-unused-local-non-trivial-variable <bugprone/unused-local-non-trivial-variable>`, :doc:`bugprone-unused-raii <bugprone/unused-raii>`, "Yes" :doc:`bugprone-unused-return-value <bugprone/unused-return-value>`, diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-to-allow-exceptions.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-to-allow-exceptions.cpp new file mode 100644 index 0000000000000..1e24a23e7afc9 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-to-allow-exceptions.cpp @@ -0,0 +1,45 @@ +// RUN: %check_clang_tidy -std=c++11,c++14 %s bugprone-unsafe-to-allow-exceptions %t -- -- -fexceptions + +struct may_throw { + may_throw(may_throw&&) throw(int) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: function 'may_throw' should not throw exceptions but it is still marked as throwable [bugprone-unsafe-to-allow-exceptions] + } + may_throw& operator=(may_throw&&) noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: function 'operator=' should not throw exceptions but it is still marked as throwable + } + ~may_throw() noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: function '~may_throw' should not throw exceptions but it is still marked as throwable + } + + void f() noexcept(false) { + } +}; + +struct no_throw { + no_throw(no_throw&&) throw() { + } + no_throw& operator=(no_throw&&) noexcept(true) { + } + ~no_throw() noexcept(true) { + } +}; + +int main() noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: function 'main' should not throw exceptions but it is still marked as throwable + return 0; +} + +void swap(int&, int&) noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'swap' should not throw exceptions but it is still marked as throwable +} + +void iter_swap(int&, int&) noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'iter_swap' should not throw exceptions but it is still marked as throwable +} + +void iter_move(int&) noexcept(false) { + // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'iter_move' should not throw exceptions but it is still marked as throwable +} + +void swap(double&, double&) { +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
