Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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)),
KnownUnannotatedAsThrowing(
Options.get("KnownUnannotatedAsThrowing", false)),
UnknownAsThrowing(Options.get("UnknownAsThrowing", false)) {
llvm::SmallVector<StringRef, 8> FunctionsThatShouldNotThrowVec,
IgnoredExceptionsVec, CheckedSwapFunctionsVec;
RawFunctionsThatShouldNotThrow.split(FunctionsThatShouldNotThrowVec, ",", -1,
Expand All @@ -57,6 +60,7 @@ ExceptionEscapeCheck::ExceptionEscapeCheck(StringRef Name,
IgnoredExceptions.insert_range(IgnoredExceptionsVec);
Tracer.ignoreExceptions(std::move(IgnoredExceptions));
Tracer.ignoreBadAlloc(true);
Tracer.assumeUnannotatedFunctionsThrow(KnownUnannotatedAsThrowing);
}

void ExceptionEscapeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Expand All @@ -68,6 +72,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, "KnownUnannotatedAsThrowing", KnownUnannotatedAsThrowing);
Options.store(Opts, "UnknownAsThrowing", UnknownAsThrowing);
}

void ExceptionEscapeCheck::registerMatchers(MatchFinder *Finder) {
Expand Down Expand Up @@ -103,22 +109,35 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
const utils::ExceptionAnalyzer::ExceptionInfo Info =
Tracer.analyze(MatchedDecl);

if (Info.getBehaviour() != utils::ExceptionAnalyzer::State::Throwing)
const auto Behaviour = Info.getBehaviour();
const bool IsThrowing =
Behaviour == utils::ExceptionAnalyzer::State::Throwing;
const bool IsUnknown = Behaviour == utils::ExceptionAnalyzer::State::Unknown;

const bool ReportUnknown =
IsUnknown &&
((KnownUnannotatedAsThrowing && Info.hasUnknownFromKnownUnannotated()) ||
(UnknownAsThrowing && Info.hasUnknownFromMissingDefinition()));

if (!(IsThrowing || ReportUnknown))
return;

diag(MatchedDecl->getLocation(), "an exception may be thrown in function "
"%0 which should not throw exceptions")
diag(MatchedDecl->getLocation(), "an exception may be thrown in function %0 "
"which should not throw exceptions")
Comment on lines +125 to +126
Copy link
Contributor

Choose a reason for hiding this comment

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

Did clang-format changed it? Seems unrelated

<< MatchedDecl;

if (Info.getExceptions().empty())
return;

const auto &[ThrowType, ThrowInfo] = *Info.getExceptions().begin();

if (ThrowInfo.Loc.isInvalid())
return;

const utils::ExceptionAnalyzer::CallStack &Stack = ThrowInfo.Stack;
diag(ThrowInfo.Loc,
"frame #0: unhandled exception of type %0 may be thrown in function %1 "
"here",
"frame #0: unhandled exception of type %0 may be thrown in function "
"%1 here",
Comment on lines +139 to +140
Copy link
Contributor

Choose a reason for hiding this comment

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

Same seems unrelated

DiagnosticIDs::Note)
<< QualType(ThrowType, 0U) << Stack.back().first;

Expand Down
3 changes: 3 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class ExceptionEscapeCheck : public ClangTidyCheck {
const bool CheckMain;
const bool CheckNothrowFunctions;

const bool KnownUnannotatedAsThrowing;
const bool UnknownAsThrowing;

llvm::StringSet<> FunctionsThatShouldNotThrow;
llvm::StringSet<> CheckedSwapFunctions;
utils::ExceptionAnalyzer Tracer;
Expand Down
13 changes: 13 additions & 0 deletions clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ ExceptionAnalyzer::ExceptionInfo &ExceptionAnalyzer::ExceptionInfo::merge(
Behaviour = State::Unknown;

ContainsUnknown = ContainsUnknown || Other.ContainsUnknown;
UnknownFromMissingDefinition =
UnknownFromMissingDefinition || Other.UnknownFromMissingDefinition;
UnknownFromKnownUnannotated =
UnknownFromKnownUnannotated || Other.UnknownFromKnownUnannotated;
ThrownExceptions.insert_range(Other.ThrownExceptions);
return *this;
}
Expand Down Expand Up @@ -484,10 +488,19 @@ 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 (AssumeUnannotatedThrowing &&
Result.getBehaviour() == State::NotThrowing && canThrow(Func)) {
auto Unknown = ExceptionInfo::createUnknown();
Unknown.markUnknownFromKnownUnannotated();
return Unknown;
}
return Result;
}

auto Result = ExceptionInfo::createUnknown();
Result.markUnknownFromMissingDefinition();
if (const auto *FPT = Func->getType()->getAs<FunctionProtoType>()) {
for (const QualType &Ex : FPT->exceptions()) {
CallStack.insert({Func, CallLoc});
Expand Down
27 changes: 26 additions & 1 deletion clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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}; }
Copy link
Member Author

Choose a reason for hiding this comment

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

I understand there are valid reasons for this function to return Throwing, but it still feels a bit counter-intuitive for something named createNonThrowing to produce a Throwing.

If this change doesn't match your preference, I'm totally fine reverting it. Thanks in advance.


/// By default the exception situation is unknown and must be
/// clarified step-wise.
Expand All @@ -67,6 +67,22 @@ class ExceptionAnalyzer {

State getBehaviour() const { return Behaviour; }

/// Unknown cause tracking.
void markUnknownFromMissingDefinition() {
UnknownFromMissingDefinition = true;
ContainsUnknown = true;
}
void markUnknownFromKnownUnannotated() {
UnknownFromKnownUnannotated = true;
ContainsUnknown = true;
}
bool hasUnknownFromMissingDefinition() const {
return UnknownFromMissingDefinition;
}
bool hasUnknownFromKnownUnannotated() const {
return UnknownFromKnownUnannotated;
}

/// Register a single exception type as recognized potential exception to be
/// thrown.
void registerException(const Type *ExceptionType,
Expand Down Expand Up @@ -124,12 +140,20 @@ class ExceptionAnalyzer {
/// after filtering.
bool ContainsUnknown;

bool UnknownFromMissingDefinition = false;
bool UnknownFromKnownUnannotated = false;

/// 'ThrownException' is empty if the 'Behaviour' is either 'NotThrowing' or
/// 'Unknown'.
Throwables ThrownExceptions;
};

ExceptionAnalyzer() = default;
/// When enabled, treat any function that is not explicitly non-throwing
/// as potentially throwing, even if its body analysis finds no throw.
void assumeUnannotatedFunctionsThrow(bool Enable) {
AssumeUnannotatedThrowing = Enable;
}

void ignoreBadAlloc(bool ShallIgnore) { IgnoreBadAlloc = ShallIgnore; }
void ignoreExceptions(llvm::StringSet<> ExceptionNames) {
Expand All @@ -154,6 +178,7 @@ class ExceptionAnalyzer {
bool IgnoreBadAlloc = true;
llvm::StringSet<> IgnoredExceptions;
llvm::DenseMap<const FunctionDecl *, ExceptionInfo> FunctionCache{32U};
bool AssumeUnannotatedThrowing = false;
};

} // namespace clang::tidy::utils
Expand Down
6 changes: 5 additions & 1 deletion clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,11 @@ Changes in existing checks
where the check wouldn't diagnose throws in arguments to functions or
constructors. Added fine-grained configuration via options
`CheckDestructors`, `CheckMoveMemberFunctions`, `CheckMain`,
`CheckedSwapFunctions`, and `CheckNothrowFunctions`.
`CheckedSwapFunctions`, and `CheckNothrowFunctions`; and added
`KnownUnannotatedAsThrowing` and `UnknownAsThrowing` 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-infinite-loop
<clang-tidy/checks/bugprone/infinite-loop>` check by adding detection for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,15 @@ Options

Comma separated list containing type names which are not counted as thrown
exceptions in the check. Default value is an empty string.

.. option:: KnownUnannotatedAsThrowing

When `true`, treat calls to functions with visible definitions that are not
explicitly declared as non-throwing (i.e. lack ``noexcept`` or ``throw()``)
as potentially throwing, even if their bodies are visible and no explicit
throw is found. Default value is `false`.

.. option:: UnknownAsThrowing

When `true`, treat calls to functions without visible definitions as
potentially throwing. Default value is `false`.
Comment on lines +75 to +85
Copy link
Contributor

@vbvictor vbvictor Nov 21, 2025

Choose a reason for hiding this comment

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

I think instead of 2 options we should make 1 option with 3 different levels:
Option - TreatFunctionsWithoutSpecificationAsThrowing
Levels: All, OnlyUndefined, None.

Because i don't see a good usecase when we want to set KnownUnannotatedAsThrowing to true but UnknownAsThrowing to false.
WDYT?
CC @firewave.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// RUN: %check_clang_tidy -std=c++11-or-later %s bugprone-exception-escape %t -- \
// RUN: -config='{"CheckOptions": { \
// RUN: "bugprone-exception-escape.KnownUnannotatedAsThrowing": true \
// RUN: }}' -- -fexceptions

void unannotated_no_throw_body() {}

void calls_unannotated() noexcept {
// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: an exception may be thrown in function 'calls_unannotated' which should not throw exceptions
unannotated_no_throw_body();
}

void extern_declared();

void calls_unknown() noexcept {
// CHECK-MESSAGES-NOT: warning:
extern_declared();
}

void definitely_nothrow() noexcept {}

void calls_nothrow() noexcept {
// CHECK-MESSAGES-NOT: warning:
definitely_nothrow();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// RUN: %check_clang_tidy -std=c++11-or-later %s bugprone-exception-escape %t -- \
// RUN: -config='{"CheckOptions": { \
// RUN: "bugprone-exception-escape.UnknownAsThrowing": true \
// RUN: }}' -- -fexceptions

void unannotated_no_throw_body() {}

void calls_unannotated() noexcept {
// CHECK-MESSAGES-NOT: warning:
unannotated_no_throw_body();
}

void extern_declared();

void calls_unknown() noexcept {
// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: an exception may be thrown in function 'calls_unknown' which should not throw exceptions
extern_declared();
}

void definitely_nothrow() noexcept {}

void calls_nothrow() noexcept {
// CHECK-MESSAGES-NOT: warning:
definitely_nothrow();
}