Skip to content

Suppress noreturn warning if last statement in a function is a throw #145166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 27, 2025
Merged
7 changes: 7 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,13 @@ Improvements to Clang's diagnostics
#GH69470, #GH59391, #GH58172, #GH46215, #GH45915, #GH45891, #GH44490,
#GH36703, #GH32903, #GH23312, #GH69874.

- Clang now avoids issuing `-Wreturn-type` warnings in some cases where
the final statement of a non-void function is a `throw` expression, or
a call to a function that is trivially known to always throw (i.e., its
body consists solely of a `throw` statement). This avoids certain
false positives in exception-heavy code, though only simple patterns
are currently recognized.


Improvements to Clang's time-trace
----------------------------------
Expand Down
7 changes: 7 additions & 0 deletions clang/include/clang/Basic/Attr.td
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,13 @@ def AnalyzerNoReturn : InheritableAttr {
let Documentation = [Undocumented];
}

def InferredNoReturn : InheritableAttr {
let Spellings = [];
let SemaHandler = 0;
let Subjects = SubjectList<[Function], ErrorDiag>;
let Documentation = [InternalOnly];
}

def Annotate : InheritableParamOrStmtAttr {
let Spellings = [Clang<"annotate">];
let Args = [StringArgument<"Annotation">, VariadicExprArgument<"Args">];
Expand Down
2 changes: 2 additions & 0 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ enum class CCEKind {
///< message.
};

void inferNoReturnAttr(Sema &S, const Decl *D);

/// Sema - This implements semantic analysis and AST building for C.
/// \nosubgrouping
class Sema final : public SemaBase {
Expand Down
24 changes: 23 additions & 1 deletion clang/lib/Sema/AnalysisBasedWarnings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ static void CheckFallThroughForBody(Sema &S, const Decl *D, const Stmt *Body,
ReturnsVoid = CBody->getFallthroughHandler() != nullptr;
else
ReturnsVoid = FD->getReturnType()->isVoidType();
HasNoReturn = FD->isNoReturn();
HasNoReturn = FD->isNoReturn() || FD->hasAttr<InferredNoReturnAttr>();
}
else if (const auto *MD = dyn_cast<ObjCMethodDecl>(D)) {
ReturnsVoid = MD->getReturnType()->isVoidType();
Expand Down Expand Up @@ -681,6 +681,28 @@ static void CheckFallThroughForBody(Sema &S, const Decl *D, const Stmt *Body,
if (CD.diag_FallThrough_HasNoReturn)
S.Diag(RBrace, CD.diag_FallThrough_HasNoReturn) << CD.FunKind;
} else if (!ReturnsVoid && CD.diag_FallThrough_ReturnsNonVoid) {
// If the final statement is a call to an always-throwing function,
// don't warn about the fall-through.
if (const auto *FD = D->getAsFunction()) {
if (const auto *CS = dyn_cast<CompoundStmt>(Body);
CS && !CS->body_empty()) {
const Stmt *LastStmt = CS->body_back();
// Unwrap ExprWithCleanups if necessary.
if (const auto *EWC = dyn_cast<ExprWithCleanups>(LastStmt)) {
LastStmt = EWC->getSubExpr();
}
if (const auto *CE = dyn_cast<CallExpr>(LastStmt)) {
if (const FunctionDecl *Callee = CE->getDirectCallee();
Callee && Callee->hasAttr<InferredNoReturnAttr>()) {
return; // Don't warn about fall-through.
}
}
// Direct throw.
if (isa<CXXThrowExpr>(LastStmt)) {
return; // Don't warn about fall-through.
}
}
}
bool NotInAllControlPaths = FallThroughType == MaybeFallThrough;
S.Diag(RBrace, CD.diag_FallThrough_ReturnsNonVoid)
<< CD.FunKind << NotInAllControlPaths;
Expand Down
5 changes: 3 additions & 2 deletions clang/lib/Sema/Sema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2434,9 +2434,10 @@ Sema::PopFunctionScopeInfo(const AnalysisBasedWarnings::Policy *WP,
OpenMP().popOpenMPFunctionRegion(Scope.get());

// Issue any analysis-based warnings.
if (WP && D)
if (WP && D) {
inferNoReturnAttr(*this, D);
AnalysisWarnings.IssueWarnings(*WP, Scope.get(), D, BlockType);
else
} else
for (const auto &PUD : Scope->PossiblyUnreachableDiags)
Diag(PUD.Loc, PUD.PD);

Expand Down
44 changes: 44 additions & 0 deletions clang/lib/Sema/SemaDeclAttr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "clang/Sema/ParsedAttr.h"
#include "clang/Sema/Scope.h"
#include "clang/Sema/ScopeInfo.h"
#include "clang/Sema/Sema.h"
#include "clang/Sema/SemaAMDGPU.h"
#include "clang/Sema/SemaARM.h"
#include "clang/Sema/SemaAVR.h"
Expand Down Expand Up @@ -1938,6 +1939,49 @@ static void handleNakedAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
D->addAttr(::new (S.Context) NakedAttr(S.Context, AL));
}

// FIXME: This is a best-effort heuristic.
// Currently only handles single throw expressions (optionally with
// ExprWithCleanups). We could expand this to perform control-flow analysis for
// more complex patterns.
static bool isKnownToAlwaysThrow(const FunctionDecl *FD) {
if (!FD->hasBody())
return false;
const Stmt *Body = FD->getBody();
const Stmt *OnlyStmt = nullptr;

if (const auto *Compound = dyn_cast<CompoundStmt>(Body)) {
if (Compound->size() != 1)
return false; // More than one statement, can't be known to always throw.
OnlyStmt = *Compound->body_begin();
} else {
OnlyStmt = Body;
}

// Unwrap ExprWithCleanups if necessary.
if (const auto *EWC = dyn_cast<ExprWithCleanups>(OnlyStmt)) {
OnlyStmt = EWC->getSubExpr();
}
// Check if the only statement is a throw expression.
return isa<CXXThrowExpr>(OnlyStmt);
}

void clang::inferNoReturnAttr(Sema &S, const Decl *D) {
auto *FD = dyn_cast<FunctionDecl>(D);
if (!FD)
return;

auto *NonConstFD = const_cast<FunctionDecl *>(FD);
DiagnosticsEngine &Diags = S.getDiagnostics();
if (Diags.isIgnored(diag::warn_falloff_nonvoid, FD->getLocation()) &&
Diags.isIgnored(diag::warn_suggest_noreturn_function, FD->getLocation()))
return;

if (!FD->hasAttr<NoReturnAttr>() && !FD->hasAttr<InferredNoReturnAttr>() &&
isKnownToAlwaysThrow(FD)) {
NonConstFD->addAttr(InferredNoReturnAttr::CreateImplicit(S.Context));
}
}

static void handleNoReturnAttr(Sema &S, Decl *D, const ParsedAttr &Attrs) {
if (hasDeclarator(D)) return;

Expand Down
46 changes: 46 additions & 0 deletions clang/test/SemaCXX/wreturn-always-throws.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// RUN: %clang_cc1 -fsyntax-only -fcxx-exceptions -fexceptions -Wreturn-type -verify %s
// expected-no-diagnostics

namespace std {
class string {
public:
string(const char*); // constructor for runtime_error
};
class runtime_error {
public:
runtime_error(const string &);
};
}

// Non-template version.

void throwError(const std::string& msg) {
throw std::runtime_error(msg);
}

int ensureZero(const int i) {
if (i == 0) return 0;
throwError("ERROR"); // no-warning
}

int alwaysThrows() {
throw std::runtime_error("This function always throws"); // no-warning
}

// Template version.

template<typename T>
void throwErrorTemplate(const T& msg) {
throw msg;
}

template <typename T>
int ensureZeroTemplate(T i) {
if (i == 0) return 0;
throwErrorTemplate("ERROR"); // no-warning
}

void testTemplates() {
throwErrorTemplate("ERROR");
(void)ensureZeroTemplate(42);
}