Skip to content

Commit 6ac286c

Browse files
authored
[clang-tidy] Improve bugprone-exception-escape: add stacktrace of escaped exception (#134375)
This PR add stacktrace of escaped exception to `bugprone-exception-escape` check. Changes: 1. Modified `ExceptionAnalyzer` and `ExceptionInfo` classes to hold stacktrace of escaped exception in `llvm::MapVector`. `llvm::MapVector` is needed to hold relative positions of functions in stack as well as have fast lookup. 2. Added new diagnostics based of `misc-no-recursion` check. Fixes #87422.
1 parent ce8c19f commit 6ac286c

File tree

8 files changed

+376
-60
lines changed

8 files changed

+376
-60
lines changed

clang-tools-extra/clang-tidy/bugprone/ExceptionEscapeCheck.cpp

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,45 @@ void ExceptionEscapeCheck::check(const MatchFinder::MatchResult &Result) {
7878
if (!MatchedDecl)
7979
return;
8080

81-
if (Tracer.analyze(MatchedDecl).getBehaviour() ==
82-
utils::ExceptionAnalyzer::State::Throwing)
83-
// FIXME: We should provide more information about the exact location where
84-
// the exception is thrown, maybe the full path the exception escapes
85-
diag(MatchedDecl->getLocation(), "an exception may be thrown in function "
86-
"%0 which should not throw exceptions")
87-
<< MatchedDecl;
81+
const utils::ExceptionAnalyzer::ExceptionInfo Info =
82+
Tracer.analyze(MatchedDecl);
83+
84+
if (Info.getBehaviour() != utils::ExceptionAnalyzer::State::Throwing)
85+
return;
86+
87+
diag(MatchedDecl->getLocation(), "an exception may be thrown in function "
88+
"%0 which should not throw exceptions")
89+
<< MatchedDecl;
90+
91+
const auto &[ThrowType, ThrowInfo] = *Info.getExceptions().begin();
92+
93+
if (ThrowInfo.Loc.isInvalid())
94+
return;
95+
96+
const utils::ExceptionAnalyzer::CallStack &Stack = ThrowInfo.Stack;
97+
diag(ThrowInfo.Loc,
98+
"frame #0: unhandled exception of type %0 may be thrown in function %1 "
99+
"here",
100+
DiagnosticIDs::Note)
101+
<< QualType(ThrowType, 0U) << Stack.back().first;
102+
103+
size_t FrameNo = 1;
104+
for (auto CurrIt = ++Stack.rbegin(), PrevIt = Stack.rbegin();
105+
CurrIt != Stack.rend(); ++CurrIt, ++PrevIt) {
106+
const FunctionDecl *CurrFunction = CurrIt->first;
107+
const FunctionDecl *PrevFunction = PrevIt->first;
108+
const SourceLocation PrevLocation = PrevIt->second;
109+
if (PrevLocation.isValid()) {
110+
diag(PrevLocation, "frame #%0: function %1 calls function %2 here",
111+
DiagnosticIDs::Note)
112+
<< FrameNo << CurrFunction << PrevFunction;
113+
} else {
114+
diag(CurrFunction->getLocation(),
115+
"frame #%0: function %1 calls function %2", DiagnosticIDs::Note)
116+
<< FrameNo << CurrFunction << PrevFunction;
117+
}
118+
++FrameNo;
119+
}
88120
}
89121

90122
} // namespace clang::tidy::bugprone

clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.cpp

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
namespace clang::tidy::utils {
1212

1313
void ExceptionAnalyzer::ExceptionInfo::registerException(
14-
const Type *ExceptionType) {
14+
const Type *ExceptionType, const ThrowInfo &ThrowInfo) {
1515
assert(ExceptionType != nullptr && "Only valid types are accepted");
1616
Behaviour = State::Throwing;
17-
ThrownExceptions.insert(ExceptionType);
17+
ThrownExceptions.insert({ExceptionType, ThrowInfo});
1818
}
1919

2020
void ExceptionAnalyzer::ExceptionInfo::registerExceptions(
@@ -356,10 +356,12 @@ static bool canThrow(const FunctionDecl *Func) {
356356
};
357357
}
358358

359-
bool ExceptionAnalyzer::ExceptionInfo::filterByCatch(
360-
const Type *HandlerTy, const ASTContext &Context) {
359+
ExceptionAnalyzer::ExceptionInfo::Throwables
360+
ExceptionAnalyzer::ExceptionInfo::filterByCatch(const Type *HandlerTy,
361+
const ASTContext &Context) {
361362
llvm::SmallVector<const Type *, 8> TypesToDelete;
362-
for (const Type *ExceptionTy : ThrownExceptions) {
363+
for (const auto &ThrownException : ThrownExceptions) {
364+
const Type *ExceptionTy = ThrownException.getFirst();
363365
CanQualType ExceptionCanTy = ExceptionTy->getCanonicalTypeUnqualified();
364366
CanQualType HandlerCanTy = HandlerTy->getCanonicalTypeUnqualified();
365367

@@ -409,11 +411,18 @@ bool ExceptionAnalyzer::ExceptionInfo::filterByCatch(
409411
}
410412
}
411413

412-
for (const Type *T : TypesToDelete)
413-
ThrownExceptions.erase(T);
414+
Throwables DeletedExceptions;
415+
416+
for (const Type *TypeToDelete : TypesToDelete) {
417+
const auto DeleteIt = ThrownExceptions.find(TypeToDelete);
418+
if (DeleteIt != ThrownExceptions.end()) {
419+
DeletedExceptions.insert(*DeleteIt);
420+
ThrownExceptions.erase(DeleteIt);
421+
}
422+
}
414423

415424
reevaluateBehaviour();
416-
return !TypesToDelete.empty();
425+
return DeletedExceptions;
417426
}
418427

419428
ExceptionAnalyzer::ExceptionInfo &
@@ -422,7 +431,8 @@ ExceptionAnalyzer::ExceptionInfo::filterIgnoredExceptions(
422431
llvm::SmallVector<const Type *, 8> TypesToDelete;
423432
// Note: Using a 'SmallSet' with 'llvm::remove_if()' is not possible.
424433
// Therefore this slightly hacky implementation is required.
425-
for (const Type *T : ThrownExceptions) {
434+
for (const auto &ThrownException : ThrownExceptions) {
435+
const Type *T = ThrownException.getFirst();
426436
if (const auto *TD = T->getAsTagDecl()) {
427437
if (TD->getDeclName().isIdentifier()) {
428438
if ((IgnoreBadAlloc &&
@@ -454,16 +464,15 @@ void ExceptionAnalyzer::ExceptionInfo::reevaluateBehaviour() {
454464
else
455465
Behaviour = State::Throwing;
456466
}
457-
458467
ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
459468
const FunctionDecl *Func, const ExceptionInfo::Throwables &Caught,
460-
llvm::SmallSet<const FunctionDecl *, 32> &CallStack) {
469+
CallStack &CallStack, SourceLocation CallLoc) {
461470
if (!Func || CallStack.contains(Func) ||
462471
(!CallStack.empty() && !canThrow(Func)))
463472
return ExceptionInfo::createNonThrowing();
464473

465474
if (const Stmt *Body = Func->getBody()) {
466-
CallStack.insert(Func);
475+
CallStack.insert({Func, CallLoc});
467476
ExceptionInfo Result = throwsException(Body, Caught, CallStack);
468477

469478
// For a constructor, we also have to check the initializers.
@@ -481,17 +490,23 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
481490

482491
auto Result = ExceptionInfo::createUnknown();
483492
if (const auto *FPT = Func->getType()->getAs<FunctionProtoType>()) {
484-
for (const QualType &Ex : FPT->exceptions())
485-
Result.registerException(Ex.getTypePtr());
493+
for (const QualType &Ex : FPT->exceptions()) {
494+
CallStack.insert({Func, CallLoc});
495+
Result.registerException(
496+
Ex.getTypePtr(),
497+
{Func->getExceptionSpecSourceRange().getBegin(), CallStack});
498+
CallStack.erase(Func);
499+
}
486500
}
487501
return Result;
488502
}
489503

490504
/// Analyzes a single statement on it's throwing behaviour. This is in principle
491505
/// possible except some 'Unknown' functions are called.
492-
ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
493-
const Stmt *St, const ExceptionInfo::Throwables &Caught,
494-
llvm::SmallSet<const FunctionDecl *, 32> &CallStack) {
506+
ExceptionAnalyzer::ExceptionInfo
507+
ExceptionAnalyzer::throwsException(const Stmt *St,
508+
const ExceptionInfo::Throwables &Caught,
509+
CallStack &CallStack) {
495510
auto Results = ExceptionInfo::createNonThrowing();
496511
if (!St)
497512
return Results;
@@ -505,7 +520,8 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
505520
->getPointeeType()
506521
->getUnqualifiedDesugaredType();
507522
Results.registerException(
508-
ThrownExpr->getType()->getUnqualifiedDesugaredType());
523+
ThrownExpr->getType()->getUnqualifiedDesugaredType(),
524+
{Throw->getBeginLoc(), CallStack});
509525
} else
510526
// A rethrow of a caught exception happens which makes it possible
511527
// to throw all exception that are caught in the 'catch' clause of
@@ -520,7 +536,7 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
520536
// Everything is caught through 'catch(...)'.
521537
if (!Catch->getExceptionDecl()) {
522538
ExceptionInfo Rethrown = throwsException(
523-
Catch->getHandlerBlock(), Uncaught.getExceptionTypes(), CallStack);
539+
Catch->getHandlerBlock(), Uncaught.getExceptions(), CallStack);
524540
Results.merge(Rethrown);
525541
Uncaught.clear();
526542
} else {
@@ -536,25 +552,26 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
536552
// thrown types (because it's sensitive to inheritance) the throwing
537553
// situation changes. First of all filter the exception types and
538554
// analyze if the baseclass-exception is rethrown.
539-
if (Uncaught.filterByCatch(
540-
CaughtType, Catch->getExceptionDecl()->getASTContext())) {
541-
ExceptionInfo::Throwables CaughtExceptions;
542-
CaughtExceptions.insert(CaughtType);
543-
ExceptionInfo Rethrown = throwsException(Catch->getHandlerBlock(),
544-
CaughtExceptions, CallStack);
555+
const ExceptionInfo::Throwables FilteredExceptions =
556+
Uncaught.filterByCatch(CaughtType,
557+
Catch->getExceptionDecl()->getASTContext());
558+
if (!FilteredExceptions.empty()) {
559+
ExceptionInfo Rethrown = throwsException(
560+
Catch->getHandlerBlock(), FilteredExceptions, CallStack);
545561
Results.merge(Rethrown);
546562
}
547563
}
548564
}
549565
Results.merge(Uncaught);
550566
} else if (const auto *Call = dyn_cast<CallExpr>(St)) {
551567
if (const FunctionDecl *Func = Call->getDirectCallee()) {
552-
ExceptionInfo Excs = throwsException(Func, Caught, CallStack);
568+
ExceptionInfo Excs =
569+
throwsException(Func, Caught, CallStack, Call->getBeginLoc());
553570
Results.merge(Excs);
554571
}
555572
} else if (const auto *Construct = dyn_cast<CXXConstructExpr>(St)) {
556-
ExceptionInfo Excs =
557-
throwsException(Construct->getConstructor(), Caught, CallStack);
573+
ExceptionInfo Excs = throwsException(Construct->getConstructor(), Caught,
574+
CallStack, Construct->getBeginLoc());
558575
Results.merge(Excs);
559576
} else if (const auto *DefaultInit = dyn_cast<CXXDefaultInitExpr>(St)) {
560577
ExceptionInfo Excs =
@@ -569,11 +586,12 @@ ExceptionAnalyzer::ExceptionInfo ExceptionAnalyzer::throwsException(
569586
}
570587
ExceptionInfo Excs = throwsException(Coro->getBody(), Caught, CallStack);
571588
Results.merge(throwsException(Coro->getExceptionHandler(),
572-
Excs.getExceptionTypes(), CallStack));
573-
for (const Type *Throwable : Excs.getExceptionTypes()) {
574-
if (const auto *ThrowableRec = Throwable->getAsCXXRecordDecl()) {
575-
ExceptionInfo DestructorExcs =
576-
throwsException(ThrowableRec->getDestructor(), Caught, CallStack);
589+
Excs.getExceptions(), CallStack));
590+
for (const auto &Exception : Excs.getExceptions()) {
591+
const Type *ExcType = Exception.getFirst();
592+
if (const CXXRecordDecl *ThrowableRec = ExcType->getAsCXXRecordDecl()) {
593+
ExceptionInfo DestructorExcs = throwsException(
594+
ThrowableRec->getDestructor(), Caught, CallStack, SourceLocation{});
577595
Results.merge(DestructorExcs);
578596
}
579597
}
@@ -593,9 +611,9 @@ ExceptionAnalyzer::analyzeImpl(const FunctionDecl *Func) {
593611
// Check if the function has already been analyzed and reuse that result.
594612
const auto CacheEntry = FunctionCache.find(Func);
595613
if (CacheEntry == FunctionCache.end()) {
596-
llvm::SmallSet<const FunctionDecl *, 32> CallStack;
597-
ExceptionList =
598-
throwsException(Func, ExceptionInfo::Throwables(), CallStack);
614+
CallStack CallStack;
615+
ExceptionList = throwsException(Func, ExceptionInfo::Throwables(),
616+
CallStack, Func->getLocation());
599617

600618
// Cache the result of the analysis. This is done prior to filtering
601619
// because it is best to keep as much information as possible.
@@ -610,7 +628,7 @@ ExceptionAnalyzer::analyzeImpl(const FunctionDecl *Func) {
610628

611629
ExceptionAnalyzer::ExceptionInfo
612630
ExceptionAnalyzer::analyzeImpl(const Stmt *Stmt) {
613-
llvm::SmallSet<const FunctionDecl *, 32> CallStack;
631+
CallStack CallStack;
614632
return throwsException(Stmt, ExceptionInfo::Throwables(), CallStack);
615633
}
616634

clang-tools-extra/clang-tidy/utils/ExceptionAnalyzer.h

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "clang/AST/ASTContext.h"
1313
#include "clang/ASTMatchers/ASTMatchFinder.h"
1414
#include "llvm/ADT/SmallSet.h"
15+
#include "llvm/ADT/SmallVector.h"
1516
#include "llvm/ADT/StringSet.h"
1617

1718
namespace clang::tidy::utils {
@@ -28,6 +29,10 @@ class ExceptionAnalyzer {
2829
///< definition.
2930
};
3031

32+
/// We use a MapVector to preserve the order of the functions in the call
33+
/// stack as well as have fast lookup.
34+
using CallStack = llvm::MapVector<const FunctionDecl *, SourceLocation>;
35+
3136
/// Bundle the gathered information about an entity like a function regarding
3237
/// it's exception behaviour. The 'NonThrowing'-state can be considered as the
3338
/// neutral element in terms of information propagation.
@@ -37,7 +42,15 @@ class ExceptionAnalyzer {
3742
/// exception at runtime.
3843
class ExceptionInfo {
3944
public:
40-
using Throwables = llvm::SmallSet<const Type *, 2>;
45+
/// Holds information about where an exception is thrown.
46+
/// First element in the call stack is analyzed function.
47+
struct ThrowInfo {
48+
SourceLocation Loc;
49+
CallStack Stack;
50+
};
51+
52+
using Throwables = llvm::SmallDenseMap<const Type *, ThrowInfo, 2>;
53+
4154
static ExceptionInfo createUnknown() { return {State::Unknown}; }
4255
static ExceptionInfo createNonThrowing() { return {State::Throwing}; }
4356

@@ -56,7 +69,8 @@ class ExceptionAnalyzer {
5669

5770
/// Register a single exception type as recognized potential exception to be
5871
/// thrown.
59-
void registerException(const Type *ExceptionType);
72+
void registerException(const Type *ExceptionType,
73+
const ThrowInfo &ThrowInfo);
6074

6175
/// Registers a `SmallVector` of exception types as recognized potential
6276
/// exceptions to be thrown.
@@ -73,8 +87,8 @@ class ExceptionAnalyzer {
7387
/// This method is useful in case 'catch' clauses are analyzed as it is
7488
/// possible to catch multiple exception types by one 'catch' if they
7589
/// are a subclass of the 'catch'ed exception type.
76-
/// Returns 'true' if some exceptions were filtered, otherwise 'false'.
77-
bool filterByCatch(const Type *HandlerTy, const ASTContext &Context);
90+
/// Returns filtered exceptions.
91+
Throwables filterByCatch(const Type *HandlerTy, const ASTContext &Context);
7892

7993
/// Filter the set of thrown exception type against a set of ignored
8094
/// types that shall not be considered in the exception analysis.
@@ -87,9 +101,9 @@ class ExceptionAnalyzer {
87101
/// neutral.
88102
void clear();
89103

90-
/// References the set of known exception types that can escape from the
104+
/// References the set of known exceptions that can escape from the
91105
/// corresponding entity.
92-
const Throwables &getExceptionTypes() const { return ThrownExceptions; }
106+
const Throwables &getExceptions() const { return ThrownExceptions; }
93107

94108
/// Signal if the there is any 'Unknown' element within the scope of
95109
/// the related entity. This might be relevant if the entity is 'Throwing'
@@ -126,14 +140,12 @@ class ExceptionAnalyzer {
126140
ExceptionInfo analyze(const Stmt *Stmt);
127141

128142
private:
129-
ExceptionInfo
130-
throwsException(const FunctionDecl *Func,
131-
const ExceptionInfo::Throwables &Caught,
132-
llvm::SmallSet<const FunctionDecl *, 32> &CallStack);
133-
ExceptionInfo
134-
throwsException(const Stmt *St, const ExceptionInfo::Throwables &Caught,
135-
llvm::SmallSet<const FunctionDecl *, 32> &CallStack);
136-
143+
ExceptionInfo throwsException(const FunctionDecl *Func,
144+
const ExceptionInfo::Throwables &Caught,
145+
CallStack &CallStack, SourceLocation CallLoc);
146+
ExceptionInfo throwsException(const Stmt *St,
147+
const ExceptionInfo::Throwables &Caught,
148+
CallStack &CallStack);
137149
ExceptionInfo analyzeImpl(const FunctionDecl *Func);
138150
ExceptionInfo analyzeImpl(const Stmt *Stmt);
139151

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ Changes in existing checks
187187
false positives on deleted constructors that cannot be used to construct
188188
objects, even if they have public or protected access.
189189

190+
- Improved :doc:`bugprone-exception-escape
191+
<clang-tidy/checks/bugprone/exception-escape>` check to print stack trace
192+
of a potentially escaped exception.
193+
190194
- Added an option to :doc:`bugprone-multi-level-implicit-pointer-conversion
191195
<clang-tidy/checks/bugprone/multi-level-implicit-pointer-conversion>` to
192196
choose whether to enable the check in C code or not.

0 commit comments

Comments
 (0)