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
