[clang-tools-extra] [clang-tidy] Add `readability-redundant-nested-if` check (PR #181558)
Daniil Dudkin via cfe-commits
cfe-commits at lists.llvm.org
Fri Apr 3 14:05:39 PDT 2026
https://github.com/unterumarmung updated https://github.com/llvm/llvm-project/pull/181558
>From e2075a039c3c1aaf2a483d4b276acd3c9ff93476 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 15 Feb 2026 20:33:54 +0300
Subject: [PATCH 1/5] [clang-tidy] Add `readability-redundant-nested-if` check
Introduce a readability check that merges nested `if`/`if constexpr` chains by
combining conditions with `&&`.
This resurrects the earlier patch at https://reviews.llvm.org/D130630.
The implementation keeps fix-its conservative around macros, preprocessor
directives, attributes, user-defined bool conversions, and comment placement in
removable nested headers. It also supports C++17 declaration conditions by
rewriting them into init-statement form when safe.
---
.../clang-tidy/readability/CMakeLists.txt | 1 +
.../readability/ReadabilityTidyModule.cpp | 3 +
.../readability/RedundantNestedIfCheck.cpp | 784 ++++++++++++++++++
.../readability/RedundantNestedIfCheck.h | 39 +
clang-tools-extra/docs/ReleaseNotes.rst | 6 +
.../docs/clang-tidy/checks/list.rst | 1 +
.../readability/redundant-nested-if.rst | 118 +++
.../readability/redundant-nested-if.cpp | 474 +++++++++++
8 files changed, 1426 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
diff --git a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
index 686e7c19d650b..220048f1f66e3 100644
--- a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
@@ -45,6 +45,7 @@ add_clang_library(clangTidyReadabilityModule STATIC
RedundantDeclarationCheck.cpp
RedundantFunctionPtrDereferenceCheck.cpp
RedundantMemberInitCheck.cpp
+ RedundantNestedIfCheck.cpp
RedundantParenthesesCheck.cpp
RedundantPreprocessorCheck.cpp
RedundantQualifiedAliasCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
index 8e9e00b23c84a..81dc2d06a9589 100644
--- a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
@@ -47,6 +47,7 @@
#include "RedundantFunctionPtrDereferenceCheck.h"
#include "RedundantInlineSpecifierCheck.h"
#include "RedundantMemberInitCheck.h"
+#include "RedundantNestedIfCheck.h"
#include "RedundantParenthesesCheck.h"
#include "RedundantPreprocessorCheck.h"
#include "RedundantQualifiedAliasCheck.h"
@@ -177,6 +178,8 @@ class ReadabilityModule : public ClangTidyModule {
"readability-redundant-string-cstr");
CheckFactories.registerCheck<RedundantStringInitCheck>(
"readability-redundant-string-init");
+ CheckFactories.registerCheck<RedundantNestedIfCheck>(
+ "readability-redundant-nested-if");
CheckFactories.registerCheck<SimplifyBooleanExprCheck>(
"readability-simplify-boolean-expr");
CheckFactories.registerCheck<SuspiciousCallArgumentCheck>(
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
new file mode 100644
index 0000000000000..2fc9c5fdc5ff3
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
@@ -0,0 +1,784 @@
+//===--- RedundantNestedIfCheck.cpp - clang-tidy -------------------------===//
+//
+// 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 "RedundantNestedIfCheck.h"
+#include "../utils/LexerUtils.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Stmt.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/FixIt.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include <optional>
+#include <string>
+#include <vector>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy {
+template <>
+struct OptionEnumMapping<
+ readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode> {
+ static llvm::ArrayRef<std::pair<
+ readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode,
+ StringRef>>
+ getEnumMapping() {
+ using Mode =
+ readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode;
+ static constexpr std::pair<Mode, StringRef> Mapping[] = {
+ {Mode::None, "None"},
+ {Mode::WarnOnly, "WarnOnly"},
+ {Mode::WarnAndFix, "WarnAndFix"},
+ };
+ return {Mapping};
+ }
+};
+} // namespace clang::tidy
+
+namespace clang::tidy::readability {
+
+static constexpr llvm::StringLiteral WarnOnDependentConstexprIfStr =
+ "WarnOnDependentConstexprIf";
+static constexpr llvm::StringLiteral UserDefinedBoolConversionModeStr =
+ "UserDefinedBoolConversionMode";
+
+namespace {
+enum class ChainHandling {
+ None,
+ WarnOnly,
+ WarnOnlyDependentConstexpr,
+ WarnAndFix,
+};
+
+enum class CombinedConditionBuildStatus {
+ Success,
+ UnsupportedCommentPlacement,
+ Failure,
+};
+
+struct CombinedConditionBuildResult {
+ CombinedConditionBuildStatus Status = CombinedConditionBuildStatus::Failure;
+ std::string Text;
+};
+} // namespace
+
+// Conjoining conditions with `&&` can change behavior when a condition relies
+// on user-defined bool conversion. Keep the check conservative and reject such
+// conditions for automatic merging.
+static bool containsUserDefinedBoolConversion(const Expr *ExprNode) {
+ if (!ExprNode)
+ return false;
+
+ if (const auto *Cast = dyn_cast<ImplicitCastExpr>(ExprNode);
+ Cast && Cast->getCastKind() == CK_UserDefinedConversion)
+ return true;
+
+ return llvm::any_of(ExprNode->children(), [](const Stmt *Child) {
+ const auto *ChildExpr = dyn_cast_or_null<Expr>(Child);
+ return ChildExpr && containsUserDefinedBoolConversion(ChildExpr);
+ });
+}
+
+static bool isConditionExpressionSafeToConjoin(
+ const Expr *Cond, RedundantNestedIfCheck::UserDefinedBoolConversionMode
+ UserBoolConversionMode) {
+ if (!Cond || Cond->isTypeDependent())
+ return false;
+ const bool HasUserDefinedBoolConversion =
+ containsUserDefinedBoolConversion(Cond);
+ if (UserBoolConversionMode !=
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix &&
+ HasUserDefinedBoolConversion) {
+ return false;
+ }
+ const Expr *Unwrapped = Cond->IgnoreParenImpCasts();
+ if (!Unwrapped)
+ return false;
+ const QualType CondType = Unwrapped->getType();
+ if (CondType.isNull())
+ return false;
+ if (CondType->isScalarType())
+ return true;
+ return UserBoolConversionMode ==
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix;
+}
+
+static std::optional<CharSourceRange>
+getConditionPayloadRange(const IfStmt *If, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (!If)
+ return std::nullopt;
+ const SourceLocation PayloadBegin =
+ Lexer::getLocForEndOfToken(If->getLParenLoc(), 0, SM, LangOpts);
+ if (PayloadBegin.isInvalid() || If->getRParenLoc().isInvalid())
+ return std::nullopt;
+
+ const CharSourceRange PayloadRange =
+ CharSourceRange::getCharRange(PayloadBegin, If->getRParenLoc());
+ const CharSourceRange FileRange =
+ Lexer::makeFileCharRange(PayloadRange, SM, LangOpts);
+ if (FileRange.isInvalid())
+ return std::nullopt;
+ return FileRange;
+}
+
+static std::optional<std::string>
+getConditionPayloadText(const IfStmt *If, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ const std::optional<CharSourceRange> PayloadRange =
+ getConditionPayloadRange(If, SM, LangOpts);
+ if (!PayloadRange)
+ return std::nullopt;
+
+ bool Invalid = false;
+ const StringRef PayloadText =
+ Lexer::getSourceText(*PayloadRange, SM, LangOpts, &Invalid);
+ if (Invalid || PayloadText.empty())
+ return std::nullopt;
+ return PayloadText.str();
+}
+
+static std::vector<utils::lexer::CommentToken>
+getCommentTokensInRange(CharSourceRange Range, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ std::vector<utils::lexer::CommentToken> Comments;
+ if (Range.isInvalid())
+ return Comments;
+
+ const CharSourceRange FileRange =
+ Lexer::makeFileCharRange(Range, SM, LangOpts);
+ if (FileRange.isInvalid())
+ return Comments;
+
+ const std::pair<FileID, unsigned> BeginLoc =
+ SM.getDecomposedLoc(FileRange.getBegin());
+ const std::pair<FileID, unsigned> EndLoc =
+ SM.getDecomposedLoc(FileRange.getEnd());
+ if (BeginLoc.first != EndLoc.first)
+ return Comments;
+
+ bool Invalid = false;
+ const StringRef Buffer = SM.getBufferData(BeginLoc.first, &Invalid);
+ if (Invalid)
+ return Comments;
+
+ const char *LexStart = Buffer.data() + BeginLoc.second;
+ Lexer TheLexer(SM.getLocForStartOfFile(BeginLoc.first), LangOpts,
+ Buffer.begin(), LexStart, Buffer.end());
+ TheLexer.SetCommentRetentionState(true);
+
+ while (true) {
+ Token Tok;
+ if (TheLexer.LexFromRawLexer(Tok))
+ break;
+ if (Tok.is(tok::eof) || Tok.getLocation() == FileRange.getEnd() ||
+ SM.isBeforeInTranslationUnit(FileRange.getEnd(), Tok.getLocation())) {
+ break;
+ }
+
+ if (!Tok.is(tok::comment))
+ continue;
+
+ const std::pair<FileID, unsigned> CommentLoc =
+ SM.getDecomposedLoc(Tok.getLocation());
+ if (CommentLoc.first != BeginLoc.first)
+ continue;
+
+ Comments.push_back(utils::lexer::CommentToken{
+ Tok.getLocation(),
+ StringRef(Buffer.begin() + CommentLoc.second, Tok.getLength()),
+ });
+ }
+
+ return Comments;
+}
+
+static bool locationInCharRange(SourceLocation Loc, CharSourceRange Range,
+ const SourceManager &SM) {
+ if (Loc.isInvalid() || Range.isInvalid())
+ return false;
+ return !SM.isBeforeInTranslationUnit(Loc, Range.getBegin()) &&
+ SM.isBeforeInTranslationUnit(Loc, Range.getEnd());
+}
+
+// Validate comments in the nested-if header we remove. Comments are fix-safe
+// only if they are all inside the condition payload, which is preserved
+// verbatim. Any other nested-header comment placement keeps the diagnostic but
+// suppresses fix-its.
+static bool hasOnlyPayloadCommentsInNestedHeader(const IfStmt *Nested,
+ const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (!Nested || !Nested->getThen())
+ return false;
+
+ const CharSourceRange HeaderRange = CharSourceRange::getCharRange(
+ Nested->getBeginLoc(), Nested->getThen()->getBeginLoc());
+ const CharSourceRange HeaderFileRange =
+ Lexer::makeFileCharRange(HeaderRange, SM, LangOpts);
+ if (HeaderFileRange.isInvalid())
+ return false;
+
+ const std::optional<CharSourceRange> PayloadRange =
+ getConditionPayloadRange(Nested, SM, LangOpts);
+ if (!PayloadRange)
+ return false;
+
+ const std::vector<utils::lexer::CommentToken> Comments =
+ getCommentTokensInRange(HeaderFileRange, SM, LangOpts);
+ return llvm::all_of(Comments, [&](const utils::lexer::CommentToken &Comment) {
+ return locationInCharRange(Comment.Loc, *PayloadRange, SM);
+ });
+}
+
+// Only an outer condition variable can be rewritten safely by moving it into
+// an init-statement and using the declared variable as the first conjunct.
+static bool canRewriteOuterConditionVariable(
+ const IfStmt *If, const LangOptions &LangOpts,
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode
+ UserBoolConversionMode) {
+ if (!If || !If->hasVarStorage() || If->hasInitStorage())
+ return false;
+ // `if (init; cond)` syntax is available in C++17 and later only.
+ if (!LangOpts.CPlusPlus17)
+ return false;
+ const auto *CondVar = If->getConditionVariable();
+ const auto *CondVarDeclStmt = If->getConditionVariableDeclStmt();
+ if (!CondVar || !CondVarDeclStmt || !CondVarDeclStmt->isSingleDecl() ||
+ CondVar->getName().empty()) {
+ return false;
+ }
+ const QualType VarType = CondVar->getType();
+ if (VarType.isNull())
+ return false;
+ if (UserBoolConversionMode !=
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix &&
+ !VarType->isScalarType()) {
+ return false;
+ }
+ return isConditionExpressionSafeToConjoin(If->getCond(),
+ UserBoolConversionMode);
+}
+
+// Accept either `if (...) if (...)` or `if (...) { if (...) }` where the
+// compound contains exactly one statement.
+static const IfStmt *getOnlyNestedIf(const Stmt *Then) {
+ if (!Then)
+ return nullptr;
+ if (const auto *NestedIf = dyn_cast<IfStmt>(Then))
+ return NestedIf;
+ const auto *Compound = dyn_cast<CompoundStmt>(Then);
+ if (!Compound || Compound->size() != 1)
+ return nullptr;
+ return dyn_cast<IfStmt>(Compound->body_front());
+}
+
+static bool
+isMergeCandidate(const IfStmt *If, bool AllowInitStorage, bool RequireConstexpr,
+ bool AllowConditionVariable, const LangOptions &LangOpts,
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode
+ UserBoolConversionMode) {
+ if (!If || !If->getThen())
+ return false;
+ if (If->isConsteval() || If->getElse())
+ return false;
+ if (!AllowInitStorage && If->hasInitStorage())
+ return false;
+ if (If->isConstexpr() != RequireConstexpr)
+ return false;
+ if (If->hasVarStorage())
+ return AllowConditionVariable && canRewriteOuterConditionVariable(
+ If, LangOpts, UserBoolConversionMode);
+
+ return If->getCond() && isConditionExpressionSafeToConjoin(
+ If->getCond(), UserBoolConversionMode);
+}
+
+static bool isMergeShapeCandidate(const IfStmt *If, bool AllowInitStorage,
+ bool RequireConstexpr,
+ bool AllowConditionVariable,
+ const LangOptions &LangOpts) {
+ if (!If || !If->getThen())
+ return false;
+ if (If->isConsteval() || If->getElse())
+ return false;
+ if (!AllowInitStorage && If->hasInitStorage())
+ return false;
+ if (If->isConstexpr() != RequireConstexpr)
+ return false;
+ if (If->hasVarStorage())
+ return AllowConditionVariable && LangOpts.CPlusPlus17;
+ return If->getCond() != nullptr;
+}
+
+// Statement attributes are attached outside of the `if` token range; removing
+// nested `if` tokens can make attribute placement invalid, so skip them.
+static bool isAttributedIf(const IfStmt *If, ASTContext &Context) {
+ if (!If)
+ return false;
+ const DynTypedNodeList Parents = Context.getParents(*If);
+ return !Parents.empty() && Parents[0].get<AttributedStmt>() != nullptr;
+}
+
+// Build the maximal top-down chain of mergeable nested if statements.
+static llvm::SmallVector<const IfStmt *>
+getMergeChain(const IfStmt *Root, ASTContext &Context,
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode
+ UserBoolConversionMode) {
+ llvm::SmallVector<const IfStmt *> Chain;
+ if (!Root)
+ return Chain;
+
+ const LangOptions &LangOpts = Context.getLangOpts();
+ const bool IsConstexpr = Root->isConstexpr();
+ if (!isMergeCandidate(Root, /*AllowInitStorage=*/true, IsConstexpr,
+ /*AllowConditionVariable=*/true, LangOpts,
+ UserBoolConversionMode) ||
+ isAttributedIf(Root, Context)) {
+ return Chain;
+ }
+
+ Chain.push_back(Root);
+ const IfStmt *Current = Root;
+ while (const IfStmt *Nested = getOnlyNestedIf(Current->getThen())) {
+ if (!isMergeCandidate(Nested, /*AllowInitStorage=*/false, IsConstexpr,
+ /*AllowConditionVariable=*/false, LangOpts,
+ UserBoolConversionMode) ||
+ isAttributedIf(Nested, Context)) {
+ break;
+ }
+ Chain.push_back(Nested);
+ Current = Nested;
+ }
+ return Chain;
+}
+
+// Warn-only mode for chains blocked specifically by user-defined bool
+// conversion in the outer condition.
+static llvm::SmallVector<const IfStmt *>
+getUserDefinedBoolWarnChain(const IfStmt *Root, ASTContext &Context) {
+ llvm::SmallVector<const IfStmt *> Chain;
+ if (!Root)
+ return Chain;
+
+ const LangOptions &LangOpts = Context.getLangOpts();
+ const bool IsConstexpr = Root->isConstexpr();
+ if (!isMergeShapeCandidate(Root, /*AllowInitStorage=*/true, IsConstexpr,
+ /*AllowConditionVariable=*/true, LangOpts) ||
+ isAttributedIf(Root, Context) ||
+ !containsUserDefinedBoolConversion(Root->getCond())) {
+ return Chain;
+ }
+
+ Chain.push_back(Root);
+ const IfStmt *Current = Root;
+ while (const IfStmt *Nested = getOnlyNestedIf(Current->getThen())) {
+ if (!isMergeCandidate(
+ Nested, /*AllowInitStorage=*/false, IsConstexpr,
+ /*AllowConditionVariable=*/false, LangOpts,
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode::None) ||
+ isAttributedIf(Nested, Context)) {
+ break;
+ }
+ Chain.push_back(Nested);
+ Current = Nested;
+ }
+
+ if (Chain.size() < 2)
+ Chain.clear();
+ return Chain;
+}
+
+// Locate the parent `if` that owns this node in its then-branch. This lets us
+// suppress duplicate diagnostics when the parent chain is already handled.
+static const IfStmt *getParentThenIf(const IfStmt *If, ASTContext &Context) {
+ if (!If)
+ return nullptr;
+
+ const DynTypedNodeList Parents = Context.getParents(*If);
+ if (Parents.empty())
+ return nullptr;
+
+ if (const auto *ParentIf = Parents[0].get<IfStmt>()) {
+ if (ParentIf->getThen() == If)
+ return ParentIf;
+ return nullptr;
+ }
+
+ const auto *ParentCompound = Parents[0].get<CompoundStmt>();
+ if (!ParentCompound || ParentCompound->size() != 1 ||
+ ParentCompound->body_front() != If) {
+ return nullptr;
+ }
+
+ const DynTypedNodeList GrandParents = Context.getParents(*ParentCompound);
+ if (GrandParents.empty())
+ return nullptr;
+
+ const auto *ParentIf = GrandParents[0].get<IfStmt>();
+ if (!ParentIf || ParentIf->getThen() != ParentCompound)
+ return nullptr;
+ return ParentIf;
+}
+
+static bool isConstantBooleanCondition(const Expr *Cond, const ASTContext &Ctx,
+ bool RequiredValue) {
+ if (!Cond || Cond->isValueDependent() || Cond->isInstantiationDependent())
+ return false;
+
+ bool Evaluated = false;
+ if (!Cond->EvaluateAsBooleanCondition(Evaluated, Ctx))
+ return false;
+ return Evaluated == RequiredValue;
+}
+
+static bool
+isConstexprChainSemanticallySafe(llvm::ArrayRef<const IfStmt *> Chain,
+ const ASTContext &Context) {
+ if (Chain.empty() || !Chain.front()->isConstexpr())
+ return true;
+
+ const bool OuterIsDependent =
+ Chain.front()->getCond()->isInstantiationDependent();
+
+ // Allow outer instantiation-dependence only when every nested condition is a
+ // non-dependent constant true expression. This preserves constexpr discard
+ // behavior for template branches.
+ for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
+ const Expr *NestedCond = Chain[Index]->getCond();
+ if (NestedCond->isInstantiationDependent())
+ return false;
+ if (OuterIsDependent &&
+ !isConstantBooleanCondition(NestedCond, Context,
+ /*RequiredValue=*/true)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// A range is unsafe for text edits if it crosses macro expansions or
+// preprocessor directives.
+static bool isUnsafeTokenRange(SourceRange Range, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (!Range.isValid())
+ return true;
+ if (Range.getBegin().isMacroID() || Range.getEnd().isMacroID())
+ return true;
+ if (Lexer::makeFileCharRange(CharSourceRange::getTokenRange(Range), SM,
+ LangOpts)
+ .isInvalid()) {
+ return true;
+ }
+ return utils::lexer::rangeContainsExpansionsOrDirectives(Range, SM, LangOpts);
+}
+
+static bool isUnsafeCharRange(CharSourceRange Range, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (Range.isInvalid())
+ return true;
+ if (Range.getBegin().isMacroID() || Range.getEnd().isMacroID())
+ return true;
+ if (Lexer::makeFileCharRange(Range, SM, LangOpts).isInvalid())
+ return true;
+ return utils::lexer::rangeContainsExpansionsOrDirectives(Range.getAsRange(),
+ SM, LangOpts);
+}
+
+// Validate every range that contributes to the final edit set before offering
+// fix-its. If any range is unsafe, keep diagnostics but do not rewrite.
+static bool isFixitSafeForChain(llvm::ArrayRef<const IfStmt *> Chain,
+ const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (Chain.empty())
+ return false;
+
+ const IfStmt *const Root = Chain.front();
+ if (Root->hasInitStorage() && !Root->hasVarStorage()) {
+ if (isUnsafeTokenRange(Chain.front()->getCond()->getSourceRange(), SM,
+ LangOpts)) {
+ return false;
+ }
+ } else {
+ const std::optional<CharSourceRange> RootConditionRange =
+ getConditionPayloadRange(Root, SM, LangOpts);
+ if (!RootConditionRange ||
+ isUnsafeCharRange(*RootConditionRange, SM, LangOpts)) {
+ return false;
+ }
+ }
+
+ if (Root->hasVarStorage()) {
+ const auto *CondVarDeclStmt = Root->getConditionVariableDeclStmt();
+ if (!CondVarDeclStmt ||
+ isUnsafeTokenRange(CondVarDeclStmt->getSourceRange(), SM, LangOpts)) {
+ return false;
+ }
+ } else if (!Root->hasInitStorage() &&
+ isUnsafeTokenRange(Root->getCond()->getSourceRange(), SM,
+ LangOpts)) {
+ return false;
+ }
+
+ for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
+ const IfStmt *const Nested = Chain[Index];
+ if (isUnsafeTokenRange(Nested->getCond()->getSourceRange(), SM, LangOpts))
+ return false;
+
+ const CharSourceRange NestedHeaderRange = CharSourceRange::getCharRange(
+ Nested->getBeginLoc(), Nested->getThen()->getBeginLoc());
+ if (isUnsafeCharRange(NestedHeaderRange, SM, LangOpts))
+ return false;
+
+ const auto *Wrapper = dyn_cast<CompoundStmt>(Chain[Index - 1]->getThen());
+ if (!Wrapper)
+ continue;
+
+ if (isUnsafeTokenRange(Wrapper->getSourceRange(), SM, LangOpts) ||
+ isUnsafeTokenRange(
+ SourceRange(Wrapper->getLBracLoc(), Wrapper->getLBracLoc()), SM,
+ LangOpts) ||
+ isUnsafeTokenRange(
+ SourceRange(Wrapper->getRBracLoc(), Wrapper->getRBracLoc()), SM,
+ LangOpts)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static CombinedConditionBuildResult
+buildCombinedCondition(llvm::ArrayRef<const IfStmt *> Chain,
+ const ASTContext &Context) {
+ CombinedConditionBuildResult Result;
+ if (Chain.empty())
+ return Result;
+
+ const SourceManager &SM = Context.getSourceManager();
+ const LangOptions &LangOpts = Context.getLangOpts();
+ std::string Combined;
+
+ for (std::size_t Index = 0; Index < Chain.size(); ++Index) {
+ const IfStmt *If = Chain[Index];
+ if (Index == 0 && If->hasVarStorage()) {
+ const auto *CondVar = If->getConditionVariable();
+ if (!CondVar)
+ return Result;
+
+ // Preserve comments and spelling in declaration conditions by using the
+ // full payload text between parentheses instead of the AST declaration
+ // source range.
+ const std::optional<std::string> CondPayloadText =
+ getConditionPayloadText(If, SM, LangOpts);
+ if (!CondPayloadText)
+ return Result;
+ Combined += *CondPayloadText;
+ Combined += "; ";
+ const llvm::StringRef CondVarName = CondVar->getName();
+ Combined.append(CondVarName.begin(), CondVarName.end());
+ continue;
+ }
+
+ std::optional<std::string> CondText;
+ if (Index == 0 && If->hasInitStorage()) {
+ const llvm::StringRef CondExprText =
+ tooling::fixit::getText(*If->getCond(), Context);
+ if (CondExprText.empty())
+ return Result;
+ CondText = CondExprText.str();
+ } else {
+ CondText = getConditionPayloadText(If, SM, LangOpts);
+ if (!CondText)
+ return Result;
+ }
+
+ // Nested headers are removed by the fix-it. Keep only comments that are
+ // semantically local to the removed header and can be preserved safely.
+ if (Index > 0 && !hasOnlyPayloadCommentsInNestedHeader(If, SM, LangOpts)) {
+ Result.Status = CombinedConditionBuildStatus::UnsupportedCommentPlacement;
+ return Result;
+ }
+
+ if (!Combined.empty())
+ Combined += " && ";
+ // Parenthesize each condition to preserve precedence in the rewritten
+ // condition regardless of original operator binding.
+ Combined += "(";
+ Combined += *CondText;
+ Combined += ")";
+ }
+ Result.Status = CombinedConditionBuildStatus::Success;
+ Result.Text = std::move(Combined);
+ return Result;
+}
+
+static std::optional<CharSourceRange>
+getConditionReplacementRange(const IfStmt *If, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ if (!If)
+ return std::nullopt;
+ if (If->hasInitStorage() && !If->hasVarStorage())
+ return CharSourceRange::getTokenRange(If->getCond()->getSourceRange());
+ return getConditionPayloadRange(If, SM, LangOpts);
+}
+
+static ChainHandling
+getChainHandling(llvm::ArrayRef<const IfStmt *> Chain,
+ const ASTContext &Context, const SourceManager &SM,
+ const LangOptions &LangOpts, bool WarnOnDependentConstexprIf,
+ std::optional<std::string> *CombinedCondition) {
+ // One place for all gating logic: chain shape, edit safety, constexpr
+ // semantic safety, and final condition synthesis.
+ if (Chain.size() < 2)
+ return ChainHandling::None;
+ if (!isFixitSafeForChain(Chain, SM, LangOpts))
+ return ChainHandling::None;
+ if (!isConstexprChainSemanticallySafe(Chain, Context)) {
+ return WarnOnDependentConstexprIf
+ ? ChainHandling::WarnOnlyDependentConstexpr
+ : ChainHandling::None;
+ }
+
+ const CombinedConditionBuildResult Combined =
+ buildCombinedCondition(Chain, Context);
+ if (Combined.Status ==
+ CombinedConditionBuildStatus::UnsupportedCommentPlacement)
+ return ChainHandling::WarnOnly;
+ if (Combined.Status != CombinedConditionBuildStatus::Success)
+ return ChainHandling::None;
+ if (CombinedCondition)
+ *CombinedCondition = Combined.Text;
+ return ChainHandling::WarnAndFix;
+}
+
+static bool
+parentWillHandleThisIf(const IfStmt *If, ASTContext &Context,
+ const SourceManager &SM, const LangOptions &LangOpts,
+ bool WarnOnDependentConstexprIf,
+ RedundantNestedIfCheck::UserDefinedBoolConversionMode
+ UserBoolConversionMode) {
+ const IfStmt *const ParentIf = getParentThenIf(If, Context);
+ if (!ParentIf)
+ return false;
+
+ const auto ParentChain =
+ getMergeChain(ParentIf, Context, UserBoolConversionMode);
+ if (ParentChain.size() < 2 || ParentChain[1] != If)
+ return false;
+
+ return getChainHandling(ParentChain, Context, SM, LangOpts,
+ WarnOnDependentConstexprIf,
+ /*CombinedCondition=*/nullptr) != ChainHandling::None;
+}
+
+static bool
+parentWillHandleUserDefinedBoolWarning(const IfStmt *If, ASTContext &Context,
+ const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ const IfStmt *const ParentIf = getParentThenIf(If, Context);
+ if (!ParentIf)
+ return false;
+
+ const auto ParentChain = getUserDefinedBoolWarnChain(ParentIf, Context);
+ if (ParentChain.size() < 2 || ParentChain[1] != If)
+ return false;
+ return isFixitSafeForChain(ParentChain, SM, LangOpts);
+}
+
+RedundantNestedIfCheck::RedundantNestedIfCheck(StringRef Name,
+ ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context), WarnOnDependentConstexprIf(Options.get(
+ WarnOnDependentConstexprIfStr, false)),
+ UserBoolConversionMode(Options.get(UserDefinedBoolConversionModeStr,
+ UserDefinedBoolConversionMode::None)) {
+}
+
+void RedundantNestedIfCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+ Options.store(Opts, WarnOnDependentConstexprIfStr,
+ WarnOnDependentConstexprIf);
+ Options.store(Opts, UserDefinedBoolConversionModeStr, UserBoolConversionMode);
+}
+
+void RedundantNestedIfCheck::registerMatchers(MatchFinder *Finder) {
+ Finder->addMatcher(ifStmt().bind("if"), this);
+}
+
+void RedundantNestedIfCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *If = Result.Nodes.getNodeAs<IfStmt>("if");
+ if (!If)
+ return;
+
+ ASTContext &Context = *Result.Context;
+ const SourceManager &SM = *Result.SourceManager;
+ const LangOptions &LangOpts = Context.getLangOpts();
+
+ const auto Chain = getMergeChain(If, Context, UserBoolConversionMode);
+ if (Chain.size() < 2) {
+ if (UserBoolConversionMode != UserDefinedBoolConversionMode::WarnOnly)
+ return;
+
+ if (parentWillHandleUserDefinedBoolWarning(If, Context, SM, LangOpts))
+ return;
+
+ const auto WarnChain = getUserDefinedBoolWarnChain(If, Context);
+ if (WarnChain.size() < 2 || !isFixitSafeForChain(WarnChain, SM, LangOpts))
+ return;
+
+ diag(If->getIfLoc(), "nested if statements can be merged");
+ return;
+ }
+ if (parentWillHandleThisIf(If, Context, SM, LangOpts,
+ WarnOnDependentConstexprIf,
+ UserBoolConversionMode)) {
+ return;
+ }
+
+ std::optional<std::string> CombinedCondition;
+ const ChainHandling Handling =
+ getChainHandling(Chain, Context, SM, LangOpts, WarnOnDependentConstexprIf,
+ &CombinedCondition);
+ if (Handling == ChainHandling::None)
+ return;
+
+ if (Handling == ChainHandling::WarnOnlyDependentConstexpr) {
+ // Keep this as diagnostic-only: the option explicitly asks for awareness
+ // in templates even when rewrite safety cannot be guaranteed.
+ diag(If->getIfLoc(),
+ "nested instantiation-dependent if constexpr statements can be "
+ "merged");
+ return;
+ }
+
+ if (Handling == ChainHandling::WarnOnly) {
+ diag(If->getIfLoc(), "nested if statements can be merged");
+ return;
+ }
+
+ auto Diag = diag(If->getIfLoc(), "nested if statements can be merged");
+ const std::optional<CharSourceRange> CondRange =
+ getConditionReplacementRange(If, SM, LangOpts);
+ if (!CondRange || !CombinedCondition)
+ return;
+ Diag << FixItHint::CreateReplacement(*CondRange, *CombinedCondition);
+
+ for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
+ const IfStmt *const Nested = Chain[Index];
+ Diag << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
+ Nested->getBeginLoc(), Nested->getThen()->getBeginLoc()));
+
+ if (const auto *Wrapper =
+ dyn_cast<CompoundStmt>(Chain[Index - 1]->getThen())) {
+ Diag << FixItHint::CreateRemoval(Wrapper->getLBracLoc())
+ << FixItHint::CreateRemoval(Wrapper->getRBracLoc());
+ }
+ }
+}
+
+} // namespace clang::tidy::readability
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
new file mode 100644
index 0000000000000..33ead69ebc2b4
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
@@ -0,0 +1,39 @@
+//===--- RedundantNestedIfCheck.h - clang-tidy -----------------*- C++ -*-===//
+//
+// 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_READABILITY_REDUNDANTNESTEDIFCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_REDUNDANTNESTEDIFCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::readability {
+
+/// Detects nested ``if`` statements that can be merged into one by
+/// concatenating conditions with ``&&``.
+class RedundantNestedIfCheck : public ClangTidyCheck {
+public:
+ enum class UserDefinedBoolConversionMode {
+ None,
+ WarnOnly,
+ WarnAndFix,
+ };
+
+ RedundantNestedIfCheck(StringRef Name, ClangTidyContext *Context);
+
+ void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+
+private:
+ const bool WarnOnDependentConstexprIf;
+ const UserDefinedBoolConversionMode UserBoolConversionMode;
+};
+
+} // namespace clang::tidy::readability
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_REDUNDANTNESTEDIFCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 69dc5b9633398..34796503c6372 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -163,6 +163,12 @@ New checks
Suggests insertion of ``std::move(...)`` to turn copy assignment operator
calls into move assignment ones, when deemed valid and profitable.
+- New :doc:`readability-redundant-nested-if
+ <clang-tidy/checks/readability/redundant-nested-if>` check.
+
+ Finds nested ``if`` statements that can be merged into a single ``if`` by
+ combining conditions with ``&&``.
+
- New :doc:`readability-redundant-qualified-alias
<clang-tidy/checks/readability/redundant-qualified-alias>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 2b5be931271ec..22c178362a5f6 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -416,6 +416,7 @@ Clang-Tidy Checks
:doc:`readability-redundant-function-ptr-dereference <readability/redundant-function-ptr-dereference>`, "Yes"
:doc:`readability-redundant-inline-specifier <readability/redundant-inline-specifier>`, "Yes"
:doc:`readability-redundant-member-init <readability/redundant-member-init>`, "Yes"
+ :doc:`readability-redundant-nested-if <readability/redundant-nested-if>`, "Yes"
:doc:`readability-redundant-parentheses <readability/redundant-parentheses>`, "Yes"
:doc:`readability-redundant-preprocessor <readability/redundant-preprocessor>`,
:doc:`readability-redundant-qualified-alias <readability/redundant-qualified-alias>`, "Yes"
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
new file mode 100644
index 0000000000000..847c5a70d0be2
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
@@ -0,0 +1,118 @@
+.. title:: clang-tidy - readability-redundant-nested-if
+
+readability-redundant-nested-if
+===============================
+
+Finds nested ``if`` statements that can be merged by combining their conditions
+with ``&&``.
+
+Example:
+
+.. code-block:: c++
+
+ if (a) {
+ if (b) {
+ work();
+ }
+ }
+
+becomes
+
+.. code-block:: c++
+
+ if ((a) && (b)) {
+ work();
+ }
+
+The check can merge longer chains as well:
+
+.. code-block:: c++
+
+ if (a) {
+ if (b) {
+ if (c) {
+ work();
+ }
+ }
+ }
+
+becomes
+
+.. code-block:: c++
+
+ if ((a) && (b) && (c)) {
+ work();
+ }
+
+The check also supports outer declaration conditions in C++17 and later:
+
+.. code-block:: c++
+
+ if (bool x = ready()) {
+ if (can_run()) {
+ work();
+ }
+ }
+
+becomes
+
+.. code-block:: c++
+
+ if (bool x = ready(); x && (can_run())) {
+ work();
+ }
+
+Safety rules
+------------
+
+The check only transforms chains where:
+
+- Neither outer nor nested ``if`` has an ``else`` branch.
+- Nested merged ``if`` statements do not use condition variables.
+- In C++17 and later, the outermost ``if`` may use a condition variable if it
+ can be rewritten to an init-statement form, for example
+ ``if (auto v = f())`` to ``if (auto v = f(); v && ...)``.
+- When the outermost statement is already in ``if (init; cond)`` form, the
+ check keeps ``init`` unchanged and merges only into ``cond``.
+- By default, merged conditions avoid user-defined ``bool`` conversions to
+ preserve short-circuit semantics. This can be changed with
+ `UserDefinedBoolConversionMode`.
+- Only the outermost ``if`` may have an init-statement.
+- No merged ``if`` is ``if consteval``.
+- All merged ``if`` statements are either all ``if constexpr`` or all regular
+ ``if``.
+- No merged ``if`` statement has statement attributes.
+- All rewritten ranges are free of macro/preprocessor-sensitive edits.
+- Fix-its are suppressed when comments in removed nested headers cannot be
+ preserved safely. Comments inside conditions are preserved, while
+ other comments between the ``ifs`` disable fix-its.
+
+For ``if constexpr``, nested merged conditions must be
+non-instantiation-dependent to avoid template semantic changes. The outermost
+condition may be instantiation-dependent when all nested merged conditions are
+constant ``true``.
+
+Options
+-------
+
+.. option:: `UserDefinedBoolConversionMode`
+
+ Controls how chains with an outer condition that relies on user-defined
+ ``bool`` conversion are handled.
+
+ - `None`
+ No diagnostic is emitted for those chains.
+ - `WarnOnly`
+ Emit diagnostics, but do not provide fix-its.
+ - `WarnAndFix`
+ Emit diagnostics and provide fix-its.
+
+ Default is `None`.
+
+.. option:: `WarnOnDependentConstexprIf`
+
+ When set to `true`, the check also emits diagnostics for remaining unsafe
+ ``if constexpr`` chains (for example, with instantiation-dependent nested
+ conditions), but does not provide a fix-it for them.
+
+ Default is `false`.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
new file mode 100644
index 0000000000000..7c26883395718
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
@@ -0,0 +1,474 @@
+// RUN: %check_clang_tidy -check-suffixes=BASE -std=c++11 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17 -std=c++17 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20 -std=c++20 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23 -std=c++23 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26 -std=c++26 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26,OPTWARN -std=c++26 %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.WarnOnDependentConstexprIf: true, readability-redundant-nested-if.UserDefinedBoolConversionMode: WarnOnly}}' -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,OPTFIX -std=c++17 %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.UserDefinedBoolConversionMode: WarnAndFix}}' -- -fno-delayed-template-parsing
+
+bool cond(int X = 0);
+void sink();
+void bar();
+struct BoolLike {
+ explicit operator bool() const;
+};
+BoolLike make_bool_like();
+
+#define INNER_IF(C) if (C) sink()
+#define COND_MACRO cond()
+#define OUTER_IF if (cond())
+
+// Core coverage under default options.
+void positive_cases() {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1)) {
+ sink();
+ }
+ }
+ // CHECK-FIXES-BASE: if ((cond()) && (cond(1)))
+ // CHECK-FIXES-BASE: sink();
+
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond() || cond(1))
+ if (cond(2))
+ sink();
+ // CHECK-FIXES-BASE: if ((cond() || cond(1)) && (cond(2)))
+ // CHECK-FIXES-BASE: sink();
+
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1))
+ if (cond(2)) {
+ sink();
+ }
+ }
+ // CHECK-FIXES-BASE: if ((cond()) && (cond(1)) && (cond(2)))
+ // CHECK-FIXES-BASE: sink();
+}
+
+void stress_long_chain_case() {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond(0)) {
+ if (cond(1)) {
+ if (cond(2)) {
+ if (cond(3)) {
+ if (cond(4)) {
+ if (cond(5)) {
+ if (cond(6)) {
+ if (cond(7)) {
+ if (cond(8)) {
+ if (cond(9)) {
+ if (cond(10)) {
+ if (cond(11))
+ sink();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ // CHECK-FIXES-BASE: if ((cond(0)) && (cond(1)) && (cond(2)) && (cond(3)) && (cond(4)) && (cond(5)) && (cond(6)) && (cond(7)) && (cond(8)) && (cond(9)) && (cond(10)) && (cond(11)))
+ // CHECK-FIXES-BASE: sink();
+}
+
+void nested_chains_are_diagnosed_once_per_chain() {
+ // CHECK-MESSAGES-BASE: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-BASE: :[[@LINE+4]]:7: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1)) {
+ sink();
+ if (cond(2)) {
+ if (cond(3))
+ sink();
+ }
+ }
+ }
+ // CHECK-FIXES-BASE: if ((cond()) && (cond(1)))
+ // CHECK-FIXES-BASE: if ((cond(2)) && (cond(3)))
+}
+
+void child_chain_is_reported_when_macro_parent_is_unfixable() {
+ // CHECK-MESSAGES-BASE: :[[@LINE+2]]:5: warning: nested if statements can be merged
+ OUTER_IF {
+ if (cond(1)) {
+ if (cond(2))
+ sink();
+ }
+ }
+ // CHECK-FIXES-BASE: OUTER_IF {
+ // CHECK-FIXES-BASE: if ((cond(1)) && (cond(2)))
+ // CHECK-FIXES-BASE: sink();
+}
+
+void negative_cases() {
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (bool B = cond()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-CXX17: if (bool B = cond(); B && (cond(1)))
+ // CHECK-FIXES-CXX17: sink();
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (bool B = cond(1))
+ sink();
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1))
+ sink();
+ else
+ sink();
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1))
+ sink();
+ } else {
+ sink();
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ sink();
+ if (cond(1))
+ sink();
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (cond(1))
+ sink();
+ sink();
+ }
+
+}
+
+// Option-specific behavior for UserDefinedBoolConversionMode.
+void user_defined_bool_conversion_mode_cases() {
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTWARN: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTFIX: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (make_bool_like()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-OPTFIX: if ((make_bool_like()) && (cond(1)))
+ // CHECK-FIXES-OPTFIX: sink();
+}
+
+void macro_and_preprocessor_cases() {
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ INNER_IF(cond(1));
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (COND_MACRO) {
+ if (cond(1))
+ sink();
+ }
+
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+#if 1
+ if (cond(1))
+ sink();
+#endif
+ }
+}
+
+void comment_handling_cases() {
+ // Comments inside condition payloads are preserved by the merged condition.
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond() /* outer payload */) {
+ if (/* inner payload */ cond(1))
+ sink();
+ }
+ // CHECK-FIXES-BASE: if ((cond() /* outer payload */) && (/* inner payload */ cond(1)))
+ // CHECK-FIXES-BASE: sink();
+
+ // Trailing comments in nested headers are warning-only (no fix-it).
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) // outer trailing
+ if (cond(1)) // inner trailing
+ sink();
+ // CHECK-FIXES-BASE: if (cond()) // outer trailing
+ // CHECK-FIXES-BASE-NEXT: if (cond(1)) // inner trailing
+ // CHECK-FIXES-BASE-NEXT: sink();
+
+ // Comments in other nested-header locations are warning-only (no fix-it).
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if /* nested header comment */ (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-BASE: if /* nested header comment */ (cond(1))
+}
+
+#if __cplusplus >= 201703L
+int side_effect();
+
+// C++17 language feature coverage.
+void init_statement_cases() {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (side_effect(); cond()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-CXX17: if (side_effect(); (cond()) && (cond(1)))
+ // CHECK-FIXES-CXX17: sink();
+
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (auto Guard = side_effect()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-CXX17: if (auto Guard = side_effect(); Guard && (cond(1)))
+ // CHECK-FIXES-CXX17: sink();
+
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond()) {
+ if (bool InnerInit = cond(1); InnerInit)
+ sink();
+ }
+
+ // Macro-expanded root conditions are diagnostic-only unsafe, so no warning
+ // with fix-it is emitted.
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (side_effect(); COND_MACRO) {
+ if (cond(1))
+ sink();
+ }
+
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (bool X = cond(); X) {
+ if (cond()) {
+ if (cond(1))
+ bar();
+ }
+ }
+ // CHECK-FIXES-CXX17: if (bool X = cond(); (X) && (cond()) && (cond(1)))
+ // CHECK-FIXES-CXX17: bar();
+
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (bool X = cond() /* here */) {
+ if (cond()) {
+ bar();
+ }
+ }
+ // CHECK-FIXES-CXX17: if (bool X = cond() /* here */; X && (cond()))
+ // CHECK-FIXES-CXX17: bar();
+}
+
+void declaration_condition_type_safety() {
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTWARN: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTFIX: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (auto Guard = make_bool_like()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-OPTFIX: if (auto Guard = make_bool_like(); Guard && (cond(1)))
+ // CHECK-FIXES-OPTFIX: sink();
+
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (bool X = cond()) {
+ if (cond()) {
+ if (cond(1))
+ bar();
+ }
+ }
+ // CHECK-FIXES-CXX17: if (bool X = cond(); X && (cond()) && (cond(1)))
+ // CHECK-FIXES-CXX17: bar();
+
+ // Macro-expanded declaration conditions are diagnostic-only unsafe, so no
+ // warning with fix-it is emitted.
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (bool X = COND_MACRO) {
+ if (cond(1))
+ sink();
+ }
+
+}
+
+constexpr bool C1 = true;
+constexpr bool C2 = false;
+
+void constexpr_non_template() {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (C1) {
+ if constexpr (C2) {
+ sink();
+ }
+ }
+ // CHECK-FIXES-CXX17: if constexpr ((C1) && (C2))
+ // CHECK-FIXES-CXX17: sink();
+}
+
+template <bool B> void dependent_constexpr_outer_is_fixable() {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (B) {
+ if constexpr (true)
+ sink();
+ }
+ // CHECK-FIXES-CXX17: if constexpr ((B) && (true))
+ // CHECK-FIXES-CXX17: sink();
+}
+
+template <bool B> void dependent_constexpr_outer_is_unsafe_when_nested_false() {
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ if constexpr (B) {
+ if constexpr (false)
+ sink();
+ }
+}
+
+template <typename T> void dependent_constexpr_operand_warn_only_under_option() {
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ if constexpr (true) {
+ if constexpr (sizeof(T) == 4)
+ sink();
+ }
+}
+
+template <typename T> void dependent_constexpr_type_chain_outer_is_fixable() {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (sizeof(T) == 4) {
+ if constexpr (true)
+ sink();
+ }
+ // CHECK-FIXES-CXX17: if constexpr ((sizeof(T) == 4) && (true))
+ // CHECK-FIXES-CXX17: sink();
+}
+
+void mixed_constexpr_and_non_constexpr(bool B) {
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (C1) {
+ if (B)
+ sink();
+ }
+
+ // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (B) {
+ if constexpr (C1)
+ sink();
+ }
+}
+
+#if __cplusplus >= 202400L
+template <typename T> void constexpr_template_static_assert() {
+ // CHECK-MESSAGES-CXX26-NOT: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ if constexpr (sizeof(T) == 1) {
+ if constexpr (false) {
+ static_assert(false, "discarded in template context");
+ }
+ }
+}
+#endif
+
+#endif
+
+#if __cplusplus >= 202002L
+void constexpr_requires_expression_cases() {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (requires { sizeof(int); }) {
+ if constexpr (requires { sizeof(long); })
+ sink();
+ }
+ // CHECK-FIXES-CXX20: if constexpr ((requires { sizeof(int); }) && (requires { sizeof(long); }))
+ // CHECK-FIXES-CXX20: sink();
+}
+
+void constexpr_init_statement_cases() {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (constexpr bool HasInt = requires { sizeof(int); }; HasInt) {
+ if constexpr (requires { sizeof(long); })
+ sink();
+ }
+ // CHECK-FIXES-CXX20: if constexpr (constexpr bool HasInt = requires { sizeof(int); }; (HasInt) && (requires { sizeof(long); }))
+ // CHECK-FIXES-CXX20: sink();
+}
+
+template <typename T> void dependent_requires_outer_is_fixable() {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (requires { typename T::type; }) {
+ if constexpr (true)
+ sink();
+ }
+ // CHECK-FIXES-CXX20: if constexpr ((requires { typename T::type; }) && (true))
+ // CHECK-FIXES-CXX20: sink();
+}
+
+void attribute_cases(bool B1, bool B2) {
+ // CHECK-MESSAGES-CXX20-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (B1) {
+ // CHECK-MESSAGES-CXX20-NOT: :[[@LINE+1]]:5: warning: nested if statements can be merged
+ [[likely]] if (B2)
+ sink();
+ }
+
+ // CHECK-MESSAGES-CXX20-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ [[likely]] if (B1) {
+ if (B2)
+ sink();
+ }
+}
+
+void still_merges_without_attributes(bool B1, bool B2) {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (B1) {
+ if (B2)
+ sink();
+ }
+ // CHECK-FIXES-CXX20: if ((B1) && (B2))
+ // CHECK-FIXES-CXX20: sink();
+}
+#endif
+
+#ifdef __cpp_if_consteval
+consteval bool compile_time_true() { return true; }
+
+void consteval_cases(bool B1, bool B2) {
+ // CHECK-MESSAGES-CXX23-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if consteval {
+ if (B1)
+ sink();
+ } else {
+ sink();
+ }
+
+ // CHECK-MESSAGES-CXX23-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (!B1) {
+ if consteval {
+ sink();
+ } else {
+ if (B2)
+ sink();
+ }
+ }
+
+}
+
+template <bool B> constexpr void consteval_nested_in_constexpr() {
+ // CHECK-MESSAGES-CXX23-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if constexpr (B) {
+ if consteval {
+ sink();
+ }
+ }
+}
+#endif
>From a8f67b04db0ab3109cabc5edee578f83accf3c34 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 15 Feb 2026 20:55:00 +0300
Subject: [PATCH 2/5] fix review comments
---
.../clang-tidy/readability/RedundantNestedIfCheck.h | 2 +-
.../checks/readability/redundant-nested-if.rst | 12 +++++++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
index 33ead69ebc2b4..4e85017a70107 100644
--- a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
@@ -1,4 +1,4 @@
-//===--- RedundantNestedIfCheck.h - clang-tidy -----------------*- C++ -*-===//
+//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
index 847c5a70d0be2..451f16eb83544 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
@@ -68,21 +68,31 @@ Safety rules
The check only transforms chains where:
- Neither outer nor nested ``if`` has an ``else`` branch.
+
- Nested merged ``if`` statements do not use condition variables.
+
- In C++17 and later, the outermost ``if`` may use a condition variable if it
can be rewritten to an init-statement form, for example
``if (auto v = f())`` to ``if (auto v = f(); v && ...)``.
+
- When the outermost statement is already in ``if (init; cond)`` form, the
check keeps ``init`` unchanged and merges only into ``cond``.
+
- By default, merged conditions avoid user-defined ``bool`` conversions to
preserve short-circuit semantics. This can be changed with
- `UserDefinedBoolConversionMode`.
+ :option:`UserDefinedBoolConversionMode`.
+
- Only the outermost ``if`` may have an init-statement.
+
- No merged ``if`` is ``if consteval``.
+
- All merged ``if`` statements are either all ``if constexpr`` or all regular
``if``.
+
- No merged ``if`` statement has statement attributes.
+
- All rewritten ranges are free of macro/preprocessor-sensitive edits.
+
- Fix-its are suppressed when comments in removed nested headers cannot be
preserved safely. Comments inside conditions are preserved, while
other comments between the ``ifs`` disable fix-its.
>From cb057da1be5b272ec2b16d8f2305e74a423b0acb Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 15 Feb 2026 21:10:31 +0300
Subject: [PATCH 3/5] fix docs build
---
.../clang-tidy/checks/readability/redundant-nested-if.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
index 451f16eb83544..51c502114b55b 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
@@ -105,7 +105,7 @@ constant ``true``.
Options
-------
-.. option:: `UserDefinedBoolConversionMode`
+.. option:: UserDefinedBoolConversionMode
Controls how chains with an outer condition that relies on user-defined
``bool`` conversion are handled.
@@ -119,7 +119,7 @@ Options
Default is `None`.
-.. option:: `WarnOnDependentConstexprIf`
+.. option:: WarnOnDependentConstexprIf
When set to `true`, the check also emits diagnostics for remaining unsafe
``if constexpr`` chains (for example, with instantiation-dependent nested
>From af00f25796a067b66dd265e654c95f9684ef919d Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Fri, 3 Apr 2026 23:59:29 +0300
Subject: [PATCH 4/5] Fix review comments
---
.../readability/RedundantNestedIfCheck.cpp | 888 ++++++++----------
.../readability/RedundantNestedIfCheck.h | 8 +-
.../readability/redundant-nested-if.rst | 40 +-
.../readability/redundant-nested-if.cpp | 190 ++--
4 files changed, 488 insertions(+), 638 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
index 2fc9c5fdc5ff3..371c5cf31b779 100644
--- a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
@@ -11,45 +11,36 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/Stmt.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Basic/Diagnostic.h"
#include "clang/Lex/Lexer.h"
#include "clang/Tooling/FixIt.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
+#include <cassert>
#include <optional>
#include <string>
#include <vector>
using namespace clang::ast_matchers;
-namespace clang::tidy {
-template <>
-struct OptionEnumMapping<
- readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode> {
- static llvm::ArrayRef<std::pair<
- readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode,
- StringRef>>
- getEnumMapping() {
- using Mode =
- readability::RedundantNestedIfCheck::UserDefinedBoolConversionMode;
- static constexpr std::pair<Mode, StringRef> Mapping[] = {
- {Mode::None, "None"},
- {Mode::WarnOnly, "WarnOnly"},
- {Mode::WarnAndFix, "WarnAndFix"},
- };
- return {Mapping};
- }
-};
-} // namespace clang::tidy
-
namespace clang::tidy::readability {
static constexpr llvm::StringLiteral WarnOnDependentConstexprIfStr =
"WarnOnDependentConstexprIf";
-static constexpr llvm::StringLiteral UserDefinedBoolConversionModeStr =
- "UserDefinedBoolConversionMode";
+static constexpr llvm::StringLiteral AllowUserDefinedBoolConversionStr =
+ "AllowUserDefinedBoolConversion";
+static constexpr llvm::StringLiteral MergeableIfDiag =
+ "nested if statements can be merged";
+static constexpr llvm::StringLiteral DependentConstexprDiag =
+ "nested instantiation-dependent if constexpr statements can be merged";
+static constexpr llvm::StringLiteral NestedIfNote =
+ "nested if statement to merge is here";
namespace {
+
+using IfChain = llvm::SmallVector<const IfStmt *>;
+
enum class ChainHandling {
None,
WarnOnly,
@@ -67,156 +58,109 @@ struct CombinedConditionBuildResult {
CombinedConditionBuildStatus Status = CombinedConditionBuildStatus::Failure;
std::string Text;
};
+
} // namespace
// Conjoining conditions with `&&` can change behavior when a condition relies
-// on user-defined bool conversion. Keep the check conservative and reject such
-// conditions for automatic merging.
-static bool containsUserDefinedBoolConversion(const Expr *ExprNode) {
- if (!ExprNode)
- return false;
+// on contextual user-defined conversion to `bool`.
+static bool containsUserDefinedBoolConversion(const Expr *Expression) {
+ assert(Expression);
- if (const auto *Cast = dyn_cast<ImplicitCastExpr>(ExprNode);
+ if (const auto *Cast = dyn_cast<ImplicitCastExpr>(Expression);
Cast && Cast->getCastKind() == CK_UserDefinedConversion)
return true;
- return llvm::any_of(ExprNode->children(), [](const Stmt *Child) {
+ return llvm::any_of(Expression->children(), [](const Stmt *Child) {
const auto *ChildExpr = dyn_cast_or_null<Expr>(Child);
return ChildExpr && containsUserDefinedBoolConversion(ChildExpr);
});
}
-static bool isConditionExpressionSafeToConjoin(
- const Expr *Cond, RedundantNestedIfCheck::UserDefinedBoolConversionMode
- UserBoolConversionMode) {
- if (!Cond || Cond->isTypeDependent())
- return false;
- const bool HasUserDefinedBoolConversion =
- containsUserDefinedBoolConversion(Cond);
- if (UserBoolConversionMode !=
- RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix &&
- HasUserDefinedBoolConversion) {
+static bool conditionNeedsBoolCast(const Expr *Condition) {
+ assert(Condition);
+
+ if (!containsUserDefinedBoolConversion(Condition))
return false;
- }
- const Expr *Unwrapped = Cond->IgnoreParenImpCasts();
+
+ const Expr *const Unwrapped =
+ Condition->IgnoreImplicitAsWritten()->IgnoreParens();
if (!Unwrapped)
+ return true;
+
+ const QualType ConditionType = Unwrapped->getType();
+ return ConditionType.isNull() || !ConditionType->isScalarType();
+}
+
+static bool
+isConditionExpressionMergeable(const Expr *Condition,
+ bool AllowUserDefinedBoolConversion) {
+ assert(Condition);
+
+ if (Condition->isTypeDependent())
return false;
- const QualType CondType = Unwrapped->getType();
- if (CondType.isNull())
+
+ if (containsUserDefinedBoolConversion(Condition))
+ return AllowUserDefinedBoolConversion;
+
+ const Expr *const Unwrapped = Condition->IgnoreParenImpCasts();
+ if (!Unwrapped)
return false;
- if (CondType->isScalarType())
- return true;
- return UserBoolConversionMode ==
- RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix;
+
+ const QualType ConditionType = Unwrapped->getType();
+ return !ConditionType.isNull() && ConditionType->isScalarType();
}
static std::optional<CharSourceRange>
-getConditionPayloadRange(const IfStmt *If, const SourceManager &SM,
- const LangOptions &LangOpts) {
- if (!If)
- return std::nullopt;
- const SourceLocation PayloadBegin =
+getIfConditionRange(const IfStmt *If, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ assert(If);
+
+ const SourceLocation ConditionBegin =
Lexer::getLocForEndOfToken(If->getLParenLoc(), 0, SM, LangOpts);
- if (PayloadBegin.isInvalid() || If->getRParenLoc().isInvalid())
+ if (ConditionBegin.isInvalid() || If->getRParenLoc().isInvalid())
return std::nullopt;
- const CharSourceRange PayloadRange =
- CharSourceRange::getCharRange(PayloadBegin, If->getRParenLoc());
+ const CharSourceRange ConditionRange =
+ CharSourceRange::getCharRange(ConditionBegin, If->getRParenLoc());
const CharSourceRange FileRange =
- Lexer::makeFileCharRange(PayloadRange, SM, LangOpts);
+ Lexer::makeFileCharRange(ConditionRange, SM, LangOpts);
if (FileRange.isInvalid())
return std::nullopt;
+
return FileRange;
}
static std::optional<std::string>
-getConditionPayloadText(const IfStmt *If, const SourceManager &SM,
- const LangOptions &LangOpts) {
- const std::optional<CharSourceRange> PayloadRange =
- getConditionPayloadRange(If, SM, LangOpts);
- if (!PayloadRange)
+getIfConditionText(const IfStmt *If, const SourceManager &SM,
+ const LangOptions &LangOpts) {
+ const std::optional<CharSourceRange> ConditionRange =
+ getIfConditionRange(If, SM, LangOpts);
+ if (!ConditionRange)
return std::nullopt;
bool Invalid = false;
- const StringRef PayloadText =
- Lexer::getSourceText(*PayloadRange, SM, LangOpts, &Invalid);
- if (Invalid || PayloadText.empty())
- return std::nullopt;
- return PayloadText.str();
-}
-
-static std::vector<utils::lexer::CommentToken>
-getCommentTokensInRange(CharSourceRange Range, const SourceManager &SM,
- const LangOptions &LangOpts) {
- std::vector<utils::lexer::CommentToken> Comments;
- if (Range.isInvalid())
- return Comments;
-
- const CharSourceRange FileRange =
- Lexer::makeFileCharRange(Range, SM, LangOpts);
- if (FileRange.isInvalid())
- return Comments;
-
- const std::pair<FileID, unsigned> BeginLoc =
- SM.getDecomposedLoc(FileRange.getBegin());
- const std::pair<FileID, unsigned> EndLoc =
- SM.getDecomposedLoc(FileRange.getEnd());
- if (BeginLoc.first != EndLoc.first)
- return Comments;
-
- bool Invalid = false;
- const StringRef Buffer = SM.getBufferData(BeginLoc.first, &Invalid);
- if (Invalid)
- return Comments;
-
- const char *LexStart = Buffer.data() + BeginLoc.second;
- Lexer TheLexer(SM.getLocForStartOfFile(BeginLoc.first), LangOpts,
- Buffer.begin(), LexStart, Buffer.end());
- TheLexer.SetCommentRetentionState(true);
-
- while (true) {
- Token Tok;
- if (TheLexer.LexFromRawLexer(Tok))
- break;
- if (Tok.is(tok::eof) || Tok.getLocation() == FileRange.getEnd() ||
- SM.isBeforeInTranslationUnit(FileRange.getEnd(), Tok.getLocation())) {
- break;
- }
-
- if (!Tok.is(tok::comment))
- continue;
-
- const std::pair<FileID, unsigned> CommentLoc =
- SM.getDecomposedLoc(Tok.getLocation());
- if (CommentLoc.first != BeginLoc.first)
- continue;
-
- Comments.push_back(utils::lexer::CommentToken{
- Tok.getLocation(),
- StringRef(Buffer.begin() + CommentLoc.second, Tok.getLength()),
- });
- }
-
- return Comments;
+ const StringRef ConditionText =
+ Lexer::getSourceText(*ConditionRange, SM, LangOpts, &Invalid);
+ return Invalid || ConditionText.empty()
+ ? std::nullopt
+ : std::optional<std::string>(ConditionText.str());
}
-static bool locationInCharRange(SourceLocation Loc, CharSourceRange Range,
- const SourceManager &SM) {
- if (Loc.isInvalid() || Range.isInvalid())
- return false;
- return !SM.isBeforeInTranslationUnit(Loc, Range.getBegin()) &&
+static bool isLocationInCharRange(SourceLocation Loc, CharSourceRange Range,
+ const SourceManager &SM) {
+ return Loc.isValid() && Range.isValid() &&
+ !SM.isBeforeInTranslationUnit(Loc, Range.getBegin()) &&
SM.isBeforeInTranslationUnit(Loc, Range.getEnd());
}
-// Validate comments in the nested-if header we remove. Comments are fix-safe
-// only if they are all inside the condition payload, which is preserved
-// verbatim. Any other nested-header comment placement keeps the diagnostic but
-// suppresses fix-its.
+// Keep fix-its only when comments in removed nested headers stay inside the
+// preserved condition text. Other comment placements keep the diagnostic but
+// suppress the rewrite.
static bool hasOnlyPayloadCommentsInNestedHeader(const IfStmt *Nested,
const SourceManager &SM,
const LangOptions &LangOpts) {
- if (!Nested || !Nested->getThen())
- return false;
+ assert(Nested);
+ assert(Nested->getThen());
const CharSourceRange HeaderRange = CharSourceRange::getCharRange(
Nested->getBeginLoc(), Nested->getThen()->getBeginLoc());
@@ -226,44 +170,38 @@ static bool hasOnlyPayloadCommentsInNestedHeader(const IfStmt *Nested,
return false;
const std::optional<CharSourceRange> PayloadRange =
- getConditionPayloadRange(Nested, SM, LangOpts);
+ getIfConditionRange(Nested, SM, LangOpts);
if (!PayloadRange)
return false;
const std::vector<utils::lexer::CommentToken> Comments =
- getCommentTokensInRange(HeaderFileRange, SM, LangOpts);
+ utils::lexer::getCommentsInRange(HeaderFileRange, SM, LangOpts);
return llvm::all_of(Comments, [&](const utils::lexer::CommentToken &Comment) {
- return locationInCharRange(Comment.Loc, *PayloadRange, SM);
+ return isLocationInCharRange(Comment.Loc, *PayloadRange, SM);
});
}
-// Only an outer condition variable can be rewritten safely by moving it into
-// an init-statement and using the declared variable as the first conjunct.
-static bool canRewriteOuterConditionVariable(
- const IfStmt *If, const LangOptions &LangOpts,
- RedundantNestedIfCheck::UserDefinedBoolConversionMode
- UserBoolConversionMode) {
- if (!If || !If->hasVarStorage() || If->hasInitStorage())
- return false;
- // `if (init; cond)` syntax is available in C++17 and later only.
- if (!LangOpts.CPlusPlus17)
- return false;
- const auto *CondVar = If->getConditionVariable();
- const auto *CondVarDeclStmt = If->getConditionVariableDeclStmt();
- if (!CondVar || !CondVarDeclStmt || !CondVarDeclStmt->isSingleDecl() ||
- CondVar->getName().empty()) {
- return false;
- }
- const QualType VarType = CondVar->getType();
- if (VarType.isNull())
+// Only an outer condition variable can be rewritten safely by moving the
+// declaration into an init-statement and conjoining the condition variable.
+static bool
+canRewriteOuterConditionVariable(const IfStmt *If,
+ bool AllowUserDefinedBoolConversion) {
+ assert(If);
+ assert(If->hasVarStorage());
+
+ if (If->hasInitStorage())
return false;
- if (UserBoolConversionMode !=
- RedundantNestedIfCheck::UserDefinedBoolConversionMode::WarnAndFix &&
- !VarType->isScalarType()) {
+
+ const auto *const ConditionVariable = If->getConditionVariable();
+ const auto *const ConditionVariableDeclStmt =
+ If->getConditionVariableDeclStmt();
+ if (!ConditionVariable || !ConditionVariableDeclStmt ||
+ !ConditionVariableDeclStmt->isSingleDecl() ||
+ ConditionVariable->getName().empty())
return false;
- }
- return isConditionExpressionSafeToConjoin(If->getCond(),
- UserBoolConversionMode);
+
+ return If->getCond() && isConditionExpressionMergeable(
+ If->getCond(), AllowUserDefinedBoolConversion);
}
// Accept either `if (...) if (...)` or `if (...) { if (...) }` where the
@@ -273,169 +211,92 @@ static const IfStmt *getOnlyNestedIf(const Stmt *Then) {
return nullptr;
if (const auto *NestedIf = dyn_cast<IfStmt>(Then))
return NestedIf;
- const auto *Compound = dyn_cast<CompoundStmt>(Then);
+
+ const auto *const Compound = dyn_cast<CompoundStmt>(Then);
if (!Compound || Compound->size() != 1)
return nullptr;
+
return dyn_cast<IfStmt>(Compound->body_front());
}
-static bool
-isMergeCandidate(const IfStmt *If, bool AllowInitStorage, bool RequireConstexpr,
- bool AllowConditionVariable, const LangOptions &LangOpts,
- RedundantNestedIfCheck::UserDefinedBoolConversionMode
- UserBoolConversionMode) {
- if (!If || !If->getThen())
- return false;
- if (If->isConsteval() || If->getElse())
- return false;
- if (!AllowInitStorage && If->hasInitStorage())
- return false;
- if (If->isConstexpr() != RequireConstexpr)
- return false;
- if (If->hasVarStorage())
- return AllowConditionVariable && canRewriteOuterConditionVariable(
- If, LangOpts, UserBoolConversionMode);
+static bool hasMergeableStructure(const IfStmt *If, bool AllowInitStorage,
+ bool RequireConstexpr) {
+ assert(If);
- return If->getCond() && isConditionExpressionSafeToConjoin(
- If->getCond(), UserBoolConversionMode);
+ return If->getThen() && !If->isConsteval() && !If->getElse() &&
+ (AllowInitStorage || !If->hasInitStorage()) &&
+ If->isConstexpr() == RequireConstexpr;
}
-static bool isMergeShapeCandidate(const IfStmt *If, bool AllowInitStorage,
- bool RequireConstexpr,
- bool AllowConditionVariable,
- const LangOptions &LangOpts) {
- if (!If || !If->getThen())
- return false;
- if (If->isConsteval() || If->getElse())
- return false;
- if (!AllowInitStorage && If->hasInitStorage())
- return false;
- if (If->isConstexpr() != RequireConstexpr)
+static bool isMergeCandidate(const IfStmt *If, bool AllowInitStorage,
+ bool RequireConstexpr, bool AllowConditionVariable,
+ bool AllowUserDefinedBoolConversion,
+ const LangOptions &LangOpts) {
+ assert(If);
+
+ if (!hasMergeableStructure(If, AllowInitStorage, RequireConstexpr))
return false;
+
if (If->hasVarStorage())
- return AllowConditionVariable && LangOpts.CPlusPlus17;
- return If->getCond() != nullptr;
+ return AllowConditionVariable && LangOpts.CPlusPlus17 &&
+ canRewriteOuterConditionVariable(If, AllowUserDefinedBoolConversion);
+
+ return If->getCond() && isConditionExpressionMergeable(
+ If->getCond(), AllowUserDefinedBoolConversion);
}
-// Statement attributes are attached outside of the `if` token range; removing
-// nested `if` tokens can make attribute placement invalid, so skip them.
+// Statement attributes wrap the `if` in an `AttributedStmt`, so removing nested
+// `if` tokens can invalidate attribute placement.
static bool isAttributedIf(const IfStmt *If, ASTContext &Context) {
- if (!If)
- return false;
+ assert(If);
+
const DynTypedNodeList Parents = Context.getParents(*If);
return !Parents.empty() && Parents[0].get<AttributedStmt>() != nullptr;
}
-// Build the maximal top-down chain of mergeable nested if statements.
-static llvm::SmallVector<const IfStmt *>
-getMergeChain(const IfStmt *Root, ASTContext &Context,
- RedundantNestedIfCheck::UserDefinedBoolConversionMode
- UserBoolConversionMode) {
- llvm::SmallVector<const IfStmt *> Chain;
- if (!Root)
- return Chain;
+static IfChain getMergeChain(const IfStmt *Root, ASTContext &Context,
+ bool AllowUserDefinedBoolConversion) {
+ assert(Root);
+ IfChain Chain;
const LangOptions &LangOpts = Context.getLangOpts();
- const bool IsConstexpr = Root->isConstexpr();
- if (!isMergeCandidate(Root, /*AllowInitStorage=*/true, IsConstexpr,
- /*AllowConditionVariable=*/true, LangOpts,
- UserBoolConversionMode) ||
+ const bool RequireConstexpr = Root->isConstexpr();
+ if (!isMergeCandidate(Root, /*AllowInitStorage=*/true,
+ /*RequireConstexpr=*/RequireConstexpr,
+ /*AllowConditionVariable=*/true,
+ AllowUserDefinedBoolConversion, LangOpts) ||
isAttributedIf(Root, Context)) {
return Chain;
}
Chain.push_back(Root);
const IfStmt *Current = Root;
- while (const IfStmt *Nested = getOnlyNestedIf(Current->getThen())) {
- if (!isMergeCandidate(Nested, /*AllowInitStorage=*/false, IsConstexpr,
- /*AllowConditionVariable=*/false, LangOpts,
- UserBoolConversionMode) ||
+ while (const IfStmt *const Nested = getOnlyNestedIf(Current->getThen())) {
+ if (!isMergeCandidate(Nested, /*AllowInitStorage=*/false,
+ /*RequireConstexpr=*/RequireConstexpr,
+ /*AllowConditionVariable=*/false,
+ AllowUserDefinedBoolConversion, LangOpts) ||
isAttributedIf(Nested, Context)) {
break;
}
- Chain.push_back(Nested);
- Current = Nested;
- }
- return Chain;
-}
-// Warn-only mode for chains blocked specifically by user-defined bool
-// conversion in the outer condition.
-static llvm::SmallVector<const IfStmt *>
-getUserDefinedBoolWarnChain(const IfStmt *Root, ASTContext &Context) {
- llvm::SmallVector<const IfStmt *> Chain;
- if (!Root)
- return Chain;
-
- const LangOptions &LangOpts = Context.getLangOpts();
- const bool IsConstexpr = Root->isConstexpr();
- if (!isMergeShapeCandidate(Root, /*AllowInitStorage=*/true, IsConstexpr,
- /*AllowConditionVariable=*/true, LangOpts) ||
- isAttributedIf(Root, Context) ||
- !containsUserDefinedBoolConversion(Root->getCond())) {
- return Chain;
- }
-
- Chain.push_back(Root);
- const IfStmt *Current = Root;
- while (const IfStmt *Nested = getOnlyNestedIf(Current->getThen())) {
- if (!isMergeCandidate(
- Nested, /*AllowInitStorage=*/false, IsConstexpr,
- /*AllowConditionVariable=*/false, LangOpts,
- RedundantNestedIfCheck::UserDefinedBoolConversionMode::None) ||
- isAttributedIf(Nested, Context)) {
- break;
- }
Chain.push_back(Nested);
Current = Nested;
}
- if (Chain.size() < 2)
- Chain.clear();
return Chain;
}
-// Locate the parent `if` that owns this node in its then-branch. This lets us
-// suppress duplicate diagnostics when the parent chain is already handled.
-static const IfStmt *getParentThenIf(const IfStmt *If, ASTContext &Context) {
- if (!If)
- return nullptr;
-
- const DynTypedNodeList Parents = Context.getParents(*If);
- if (Parents.empty())
- return nullptr;
-
- if (const auto *ParentIf = Parents[0].get<IfStmt>()) {
- if (ParentIf->getThen() == If)
- return ParentIf;
- return nullptr;
- }
-
- const auto *ParentCompound = Parents[0].get<CompoundStmt>();
- if (!ParentCompound || ParentCompound->size() != 1 ||
- ParentCompound->body_front() != If) {
- return nullptr;
- }
-
- const DynTypedNodeList GrandParents = Context.getParents(*ParentCompound);
- if (GrandParents.empty())
- return nullptr;
-
- const auto *ParentIf = GrandParents[0].get<IfStmt>();
- if (!ParentIf || ParentIf->getThen() != ParentCompound)
- return nullptr;
- return ParentIf;
-}
-
-static bool isConstantBooleanCondition(const Expr *Cond, const ASTContext &Ctx,
+static bool isConstantBooleanCondition(const Expr *Condition,
+ const ASTContext &Context,
bool RequiredValue) {
- if (!Cond || Cond->isValueDependent() || Cond->isInstantiationDependent())
+ if (!Condition || Condition->isValueDependent() ||
+ Condition->isInstantiationDependent())
return false;
- bool Evaluated = false;
- if (!Cond->EvaluateAsBooleanCondition(Evaluated, Ctx))
- return false;
- return Evaluated == RequiredValue;
+ bool EvaluatedValue = false;
+ return Condition->EvaluateAsBooleanCondition(EvaluatedValue, Context) &&
+ EvaluatedValue == RequiredValue;
}
static bool
@@ -448,51 +309,46 @@ isConstexprChainSemanticallySafe(llvm::ArrayRef<const IfStmt *> Chain,
Chain.front()->getCond()->isInstantiationDependent();
// Allow outer instantiation-dependence only when every nested condition is a
- // non-dependent constant true expression. This preserves constexpr discard
- // behavior for template branches.
- for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
- const Expr *NestedCond = Chain[Index]->getCond();
- if (NestedCond->isInstantiationDependent())
+ // non-dependent constant `true` expression. This preserves discarded-branch
+ // behavior for template-dependent `if constexpr`.
+ return llvm::all_of(llvm::drop_begin(Chain), [&](const IfStmt *Nested) {
+ const Expr *const NestedCondition = Nested->getCond();
+ if (NestedCondition->isInstantiationDependent())
return false;
- if (OuterIsDependent &&
- !isConstantBooleanCondition(NestedCond, Context,
- /*RequiredValue=*/true)) {
- return false;
- }
- }
- return true;
+ return !OuterIsDependent ||
+ isConstantBooleanCondition(NestedCondition, Context,
+ /*RequiredValue=*/true);
+ });
}
// A range is unsafe for text edits if it crosses macro expansions or
// preprocessor directives.
+template <typename RangeT>
+static bool isUnsafeRange(RangeT Range, CharSourceRange FileRange,
+ bool ContainsExpansionsOrDirectives) {
+ return Range.isInvalid() || Range.getBegin().isMacroID() ||
+ Range.getEnd().isMacroID() || FileRange.isInvalid() ||
+ ContainsExpansionsOrDirectives;
+}
+
static bool isUnsafeTokenRange(SourceRange Range, const SourceManager &SM,
const LangOptions &LangOpts) {
- if (!Range.isValid())
- return true;
- if (Range.getBegin().isMacroID() || Range.getEnd().isMacroID())
- return true;
- if (Lexer::makeFileCharRange(CharSourceRange::getTokenRange(Range), SM,
- LangOpts)
- .isInvalid()) {
- return true;
- }
- return utils::lexer::rangeContainsExpansionsOrDirectives(Range, SM, LangOpts);
+ return isUnsafeRange(
+ Range,
+ Lexer::makeFileCharRange(CharSourceRange::getTokenRange(Range), SM,
+ LangOpts),
+ utils::lexer::rangeContainsExpansionsOrDirectives(Range, SM, LangOpts));
}
static bool isUnsafeCharRange(CharSourceRange Range, const SourceManager &SM,
const LangOptions &LangOpts) {
- if (Range.isInvalid())
- return true;
- if (Range.getBegin().isMacroID() || Range.getEnd().isMacroID())
- return true;
- if (Lexer::makeFileCharRange(Range, SM, LangOpts).isInvalid())
- return true;
- return utils::lexer::rangeContainsExpansionsOrDirectives(Range.getAsRange(),
- SM, LangOpts);
+ return isUnsafeRange(Range, Lexer::makeFileCharRange(Range, SM, LangOpts),
+ utils::lexer::rangeContainsExpansionsOrDirectives(
+ Range.getAsRange(), SM, LangOpts));
}
// Validate every range that contributes to the final edit set before offering
-// fix-its. If any range is unsafe, keep diagnostics but do not rewrite.
+// fix-its. If any range is unsafe, keep looking for a diagnosable child chain.
static bool isFixitSafeForChain(llvm::ArrayRef<const IfStmt *> Chain,
const SourceManager &SM,
const LangOptions &LangOpts) {
@@ -500,133 +356,148 @@ static bool isFixitSafeForChain(llvm::ArrayRef<const IfStmt *> Chain,
return false;
const IfStmt *const Root = Chain.front();
- if (Root->hasInitStorage() && !Root->hasVarStorage()) {
- if (isUnsafeTokenRange(Chain.front()->getCond()->getSourceRange(), SM,
- LangOpts)) {
- return false;
- }
- } else {
- const std::optional<CharSourceRange> RootConditionRange =
- getConditionPayloadRange(Root, SM, LangOpts);
- if (!RootConditionRange ||
- isUnsafeCharRange(*RootConditionRange, SM, LangOpts)) {
- return false;
- }
- }
+ const std::optional<CharSourceRange> RootConditionRange =
+ Root->hasInitStorage() && !Root->hasVarStorage()
+ ? std::optional<CharSourceRange>(CharSourceRange::getTokenRange(
+ Root->getCond()->getSourceRange()))
+ : getIfConditionRange(Root, SM, LangOpts);
+ if (!RootConditionRange ||
+ isUnsafeCharRange(*RootConditionRange, SM, LangOpts))
+ return false;
+ if (!Root->hasVarStorage() &&
+ isUnsafeTokenRange(Root->getCond()->getSourceRange(), SM, LangOpts))
+ return false;
if (Root->hasVarStorage()) {
- const auto *CondVarDeclStmt = Root->getConditionVariableDeclStmt();
- if (!CondVarDeclStmt ||
- isUnsafeTokenRange(CondVarDeclStmt->getSourceRange(), SM, LangOpts)) {
+ const auto *const ConditionVariableDeclStmt =
+ Root->getConditionVariableDeclStmt();
+ if (!ConditionVariableDeclStmt ||
+ isUnsafeTokenRange(ConditionVariableDeclStmt->getSourceRange(), SM,
+ LangOpts)) {
return false;
}
- } else if (!Root->hasInitStorage() &&
- isUnsafeTokenRange(Root->getCond()->getSourceRange(), SM,
- LangOpts)) {
- return false;
}
- for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
- const IfStmt *const Nested = Chain[Index];
- if (isUnsafeTokenRange(Nested->getCond()->getSourceRange(), SM, LangOpts))
- return false;
+ const llvm::ArrayRef<const IfStmt *> ChainRef(Chain);
+ return llvm::all_of(
+ llvm::zip(ChainRef.drop_back(), ChainRef.drop_front()),
+ [&](const auto &ParentAndChild) {
+ const auto &[Parent, Child] = ParentAndChild;
+ if (isUnsafeTokenRange(Child->getCond()->getSourceRange(), SM,
+ LangOpts))
+ return false;
+
+ const CharSourceRange ChildHeaderRange = CharSourceRange::getCharRange(
+ Child->getBeginLoc(), Child->getThen()->getBeginLoc());
+ if (isUnsafeCharRange(ChildHeaderRange, SM, LangOpts))
+ return false;
+
+ const auto *const Wrapper = dyn_cast<CompoundStmt>(Parent->getThen());
+ return !Wrapper ||
+ (!isUnsafeTokenRange(
+ SourceRange(Wrapper->getLBracLoc(), Wrapper->getLBracLoc()),
+ SM, LangOpts) &&
+ !isUnsafeTokenRange(
+ SourceRange(Wrapper->getRBracLoc(), Wrapper->getRBracLoc()),
+ SM, LangOpts));
+ });
+}
- const CharSourceRange NestedHeaderRange = CharSourceRange::getCharRange(
- Nested->getBeginLoc(), Nested->getThen()->getBeginLoc());
- if (isUnsafeCharRange(NestedHeaderRange, SM, LangOpts))
- return false;
+static std::string wrapConditionText(StringRef ConditionText,
+ bool NeedBoolCast) {
+ if (!NeedBoolCast)
+ return ConditionText.str();
- const auto *Wrapper = dyn_cast<CompoundStmt>(Chain[Index - 1]->getThen());
- if (!Wrapper)
- continue;
+ std::string Result("static_cast<bool>(");
+ Result += ConditionText;
+ Result += ")";
+ return Result;
+}
- if (isUnsafeTokenRange(Wrapper->getSourceRange(), SM, LangOpts) ||
- isUnsafeTokenRange(
- SourceRange(Wrapper->getLBracLoc(), Wrapper->getLBracLoc()), SM,
- LangOpts) ||
- isUnsafeTokenRange(
- SourceRange(Wrapper->getRBracLoc(), Wrapper->getRBracLoc()), SM,
- LangOpts)) {
- return false;
- }
+static std::optional<std::string> getConjunctText(const IfStmt *If,
+ const ASTContext &Context,
+ bool UseConditionExprText) {
+ assert(If);
+
+ const SourceManager &SM = Context.getSourceManager();
+ const LangOptions &LangOpts = Context.getLangOpts();
+
+ std::optional<std::string> ConditionText;
+ if (UseConditionExprText) {
+ const StringRef ConditionExprText =
+ tooling::fixit::getText(*If->getCond(), Context);
+ if (ConditionExprText.empty())
+ return std::nullopt;
+ ConditionText = ConditionExprText.str();
+ } else {
+ ConditionText = getIfConditionText(If, SM, LangOpts);
+ if (!ConditionText)
+ return std::nullopt;
}
- return true;
+ return wrapConditionText(*ConditionText,
+ conditionNeedsBoolCast(If->getCond()));
}
static CombinedConditionBuildResult
buildCombinedCondition(llvm::ArrayRef<const IfStmt *> Chain,
const ASTContext &Context) {
- CombinedConditionBuildResult Result;
if (Chain.empty())
- return Result;
+ return {};
const SourceManager &SM = Context.getSourceManager();
const LangOptions &LangOpts = Context.getLangOpts();
- std::string Combined;
-
- for (std::size_t Index = 0; Index < Chain.size(); ++Index) {
- const IfStmt *If = Chain[Index];
- if (Index == 0 && If->hasVarStorage()) {
- const auto *CondVar = If->getConditionVariable();
- if (!CondVar)
- return Result;
-
- // Preserve comments and spelling in declaration conditions by using the
- // full payload text between parentheses instead of the AST declaration
- // source range.
- const std::optional<std::string> CondPayloadText =
- getConditionPayloadText(If, SM, LangOpts);
- if (!CondPayloadText)
- return Result;
- Combined += *CondPayloadText;
- Combined += "; ";
- const llvm::StringRef CondVarName = CondVar->getName();
- Combined.append(CondVarName.begin(), CondVarName.end());
- continue;
- }
+ std::string CombinedCondition;
- std::optional<std::string> CondText;
- if (Index == 0 && If->hasInitStorage()) {
- const llvm::StringRef CondExprText =
- tooling::fixit::getText(*If->getCond(), Context);
- if (CondExprText.empty())
- return Result;
- CondText = CondExprText.str();
- } else {
- CondText = getConditionPayloadText(If, SM, LangOpts);
- if (!CondText)
- return Result;
+ for (const auto &[Index, If] : llvm::enumerate(Chain)) {
+ const bool IsRoot = Index == 0;
+ if (!IsRoot && !hasOnlyPayloadCommentsInNestedHeader(If, SM, LangOpts)) {
+ return {CombinedConditionBuildStatus::UnsupportedCommentPlacement, {}};
}
- // Nested headers are removed by the fix-it. Keep only comments that are
- // semantically local to the removed header and can be preserved safely.
- if (Index > 0 && !hasOnlyPayloadCommentsInNestedHeader(If, SM, LangOpts)) {
- Result.Status = CombinedConditionBuildStatus::UnsupportedCommentPlacement;
- return Result;
+ if (IsRoot && If->hasVarStorage()) {
+ const auto *const ConditionVariable = If->getConditionVariable();
+ if (!ConditionVariable)
+ return {};
+
+ const std::optional<std::string> ConditionText =
+ getIfConditionText(If, SM, LangOpts);
+ if (!ConditionText)
+ return {};
+
+ CombinedCondition = *ConditionText;
+ CombinedCondition += "; ";
+ CombinedCondition += wrapConditionText(
+ ConditionVariable->getName(), conditionNeedsBoolCast(If->getCond()));
+ continue;
}
- if (!Combined.empty())
- Combined += " && ";
- // Parenthesize each condition to preserve precedence in the rewritten
- // condition regardless of original operator binding.
- Combined += "(";
- Combined += *CondText;
- Combined += ")";
+ const std::optional<std::string> ConjunctText =
+ getConjunctText(If, Context,
+ /*UseConditionExprText=*/IsRoot &&
+ If->hasInitStorage() && !If->hasVarStorage());
+ if (!ConjunctText)
+ return {};
+
+ if (!CombinedCondition.empty())
+ CombinedCondition += " && ";
+ CombinedCondition += '(';
+ CombinedCondition += *ConjunctText;
+ CombinedCondition += ')';
}
- Result.Status = CombinedConditionBuildStatus::Success;
- Result.Text = std::move(Combined);
- return Result;
+
+ return {CombinedConditionBuildStatus::Success, std::move(CombinedCondition)};
}
static std::optional<CharSourceRange>
getConditionReplacementRange(const IfStmt *If, const SourceManager &SM,
const LangOptions &LangOpts) {
- if (!If)
- return std::nullopt;
- if (If->hasInitStorage() && !If->hasVarStorage())
- return CharSourceRange::getTokenRange(If->getCond()->getSourceRange());
- return getConditionPayloadRange(If, SM, LangOpts);
+ assert(If);
+
+ return If->hasInitStorage() && !If->hasVarStorage()
+ ? std::optional<CharSourceRange>(CharSourceRange::getTokenRange(
+ If->getCond()->getSourceRange()))
+ : getIfConditionRange(If, SM, LangOpts);
}
static ChainHandling
@@ -634,12 +505,9 @@ getChainHandling(llvm::ArrayRef<const IfStmt *> Chain,
const ASTContext &Context, const SourceManager &SM,
const LangOptions &LangOpts, bool WarnOnDependentConstexprIf,
std::optional<std::string> *CombinedCondition) {
- // One place for all gating logic: chain shape, edit safety, constexpr
- // semantic safety, and final condition synthesis.
- if (Chain.size() < 2)
- return ChainHandling::None;
- if (!isFixitSafeForChain(Chain, SM, LangOpts))
+ if (Chain.size() < 2 || !isFixitSafeForChain(Chain, SM, LangOpts))
return ChainHandling::None;
+
if (!isConstexprChainSemanticallySafe(Chain, Context)) {
return WarnOnDependentConstexprIf
? ChainHandling::WarnOnlyDependentConstexpr
@@ -653,132 +521,122 @@ getChainHandling(llvm::ArrayRef<const IfStmt *> Chain,
return ChainHandling::WarnOnly;
if (Combined.Status != CombinedConditionBuildStatus::Success)
return ChainHandling::None;
+
if (CombinedCondition)
*CombinedCondition = Combined.Text;
return ChainHandling::WarnAndFix;
}
-static bool
-parentWillHandleThisIf(const IfStmt *If, ASTContext &Context,
- const SourceManager &SM, const LangOptions &LangOpts,
- bool WarnOnDependentConstexprIf,
- RedundantNestedIfCheck::UserDefinedBoolConversionMode
- UserBoolConversionMode) {
- const IfStmt *const ParentIf = getParentThenIf(If, Context);
- if (!ParentIf)
- return false;
-
- const auto ParentChain =
- getMergeChain(ParentIf, Context, UserBoolConversionMode);
- if (ParentChain.size() < 2 || ParentChain[1] != If)
- return false;
+static void emitNestedIfNotes(RedundantNestedIfCheck &Check,
+ llvm::ArrayRef<const IfStmt *> Chain) {
+ for (const IfStmt *Nested : llvm::drop_begin(Chain))
+ Check.diag(Nested->getIfLoc(), NestedIfNote, DiagnosticIDs::Note);
+}
- return getChainHandling(ParentChain, Context, SM, LangOpts,
- WarnOnDependentConstexprIf,
- /*CombinedCondition=*/nullptr) != ChainHandling::None;
+static void diagnoseChain(RedundantNestedIfCheck &Check, const IfStmt *If,
+ ASTContext &Context, bool WarnOnDependentConstexprIf,
+ bool AllowUserDefinedBoolConversion);
+
+static void diagnoseChildChain(RedundantNestedIfCheck &Check,
+ const Stmt *Branch, ASTContext &Context,
+ bool WarnOnDependentConstexprIf,
+ bool AllowUserDefinedBoolConversion) {
+ if (const IfStmt *const Nested = getOnlyNestedIf(Branch))
+ diagnoseChain(Check, Nested, Context, WarnOnDependentConstexprIf,
+ AllowUserDefinedBoolConversion);
}
-static bool
-parentWillHandleUserDefinedBoolWarning(const IfStmt *If, ASTContext &Context,
- const SourceManager &SM,
- const LangOptions &LangOpts) {
- const IfStmt *const ParentIf = getParentThenIf(If, Context);
- if (!ParentIf)
- return false;
+// Match only syntactic chain roots. If a root cannot be diagnosed because it is
+// unsafe to rewrite, descend into excluded single-child nested `if` statements
+// in both branches and try again there.
+static void diagnoseChain(RedundantNestedIfCheck &Check, const IfStmt *If,
+ ASTContext &Context, bool WarnOnDependentConstexprIf,
+ bool AllowUserDefinedBoolConversion) {
+ assert(If);
- const auto ParentChain = getUserDefinedBoolWarnChain(ParentIf, Context);
- if (ParentChain.size() < 2 || ParentChain[1] != If)
- return false;
- return isFixitSafeForChain(ParentChain, SM, LangOpts);
+ const SourceManager &SM = Context.getSourceManager();
+ const LangOptions &LangOpts = Context.getLangOpts();
+ const IfChain Chain =
+ getMergeChain(If, Context, AllowUserDefinedBoolConversion);
+
+ std::optional<std::string> CombinedCondition;
+ const ChainHandling Handling =
+ getChainHandling(Chain, Context, SM, LangOpts, WarnOnDependentConstexprIf,
+ &CombinedCondition);
+ if (Handling == ChainHandling::None) {
+ diagnoseChildChain(Check, If->getThen(), Context,
+ WarnOnDependentConstexprIf,
+ AllowUserDefinedBoolConversion);
+ diagnoseChildChain(Check, If->getElse(), Context,
+ WarnOnDependentConstexprIf,
+ AllowUserDefinedBoolConversion);
+ return;
+ }
+
+ if (Handling == ChainHandling::WarnOnlyDependentConstexpr) {
+ Check.diag(If->getIfLoc(), DependentConstexprDiag);
+ emitNestedIfNotes(Check, Chain);
+ return;
+ }
+
+ {
+ const DiagnosticBuilder Diag = Check.diag(If->getIfLoc(), MergeableIfDiag);
+ if (Handling == ChainHandling::WarnAndFix) {
+ const std::optional<CharSourceRange> ConditionRange =
+ getConditionReplacementRange(If, SM, LangOpts);
+ if (!ConditionRange || !CombinedCondition)
+ return;
+
+ Diag << FixItHint::CreateReplacement(*ConditionRange, *CombinedCondition);
+ const llvm::ArrayRef<const IfStmt *> ChainRef(Chain);
+ for (const auto &[Parent, Child] :
+ llvm::zip(ChainRef.drop_back(), ChainRef.drop_front())) {
+ Diag << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
+ Child->getBeginLoc(), Child->getThen()->getBeginLoc()));
+
+ const auto *const Wrapper = dyn_cast<CompoundStmt>(Parent->getThen());
+ if (!Wrapper)
+ continue;
+
+ Diag << FixItHint::CreateRemoval(Wrapper->getLBracLoc())
+ << FixItHint::CreateRemoval(Wrapper->getRBracLoc());
+ }
+ }
+ }
+
+ emitNestedIfNotes(Check, Chain);
}
RedundantNestedIfCheck::RedundantNestedIfCheck(StringRef Name,
ClangTidyContext *Context)
: ClangTidyCheck(Name, Context), WarnOnDependentConstexprIf(Options.get(
WarnOnDependentConstexprIfStr, false)),
- UserBoolConversionMode(Options.get(UserDefinedBoolConversionModeStr,
- UserDefinedBoolConversionMode::None)) {
-}
+ AllowUserDefinedBoolConversion(
+ Options.get(AllowUserDefinedBoolConversionStr, false)) {}
void RedundantNestedIfCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, WarnOnDependentConstexprIfStr,
WarnOnDependentConstexprIf);
- Options.store(Opts, UserDefinedBoolConversionModeStr, UserBoolConversionMode);
+ Options.store(Opts, AllowUserDefinedBoolConversionStr,
+ AllowUserDefinedBoolConversion);
}
void RedundantNestedIfCheck::registerMatchers(MatchFinder *Finder) {
- Finder->addMatcher(ifStmt().bind("if"), this);
+ Finder->addMatcher(
+ ifStmt(unless(anyOf(hasParent(ifStmt()),
+ hasParent(compoundStmt(statementCountIs(1),
+ hasParent(ifStmt()))))))
+ .bind("if"),
+ this);
}
void RedundantNestedIfCheck::check(const MatchFinder::MatchResult &Result) {
- const auto *If = Result.Nodes.getNodeAs<IfStmt>("if");
- if (!If)
- return;
+ const auto *const If = Result.Nodes.getNodeAs<IfStmt>("if");
+ assert(If);
ASTContext &Context = *Result.Context;
- const SourceManager &SM = *Result.SourceManager;
- const LangOptions &LangOpts = Context.getLangOpts();
-
- const auto Chain = getMergeChain(If, Context, UserBoolConversionMode);
- if (Chain.size() < 2) {
- if (UserBoolConversionMode != UserDefinedBoolConversionMode::WarnOnly)
- return;
-
- if (parentWillHandleUserDefinedBoolWarning(If, Context, SM, LangOpts))
- return;
-
- const auto WarnChain = getUserDefinedBoolWarnChain(If, Context);
- if (WarnChain.size() < 2 || !isFixitSafeForChain(WarnChain, SM, LangOpts))
- return;
-
- diag(If->getIfLoc(), "nested if statements can be merged");
- return;
- }
- if (parentWillHandleThisIf(If, Context, SM, LangOpts,
- WarnOnDependentConstexprIf,
- UserBoolConversionMode)) {
- return;
- }
-
- std::optional<std::string> CombinedCondition;
- const ChainHandling Handling =
- getChainHandling(Chain, Context, SM, LangOpts, WarnOnDependentConstexprIf,
- &CombinedCondition);
- if (Handling == ChainHandling::None)
- return;
-
- if (Handling == ChainHandling::WarnOnlyDependentConstexpr) {
- // Keep this as diagnostic-only: the option explicitly asks for awareness
- // in templates even when rewrite safety cannot be guaranteed.
- diag(If->getIfLoc(),
- "nested instantiation-dependent if constexpr statements can be "
- "merged");
- return;
- }
-
- if (Handling == ChainHandling::WarnOnly) {
- diag(If->getIfLoc(), "nested if statements can be merged");
- return;
- }
-
- auto Diag = diag(If->getIfLoc(), "nested if statements can be merged");
- const std::optional<CharSourceRange> CondRange =
- getConditionReplacementRange(If, SM, LangOpts);
- if (!CondRange || !CombinedCondition)
- return;
- Diag << FixItHint::CreateReplacement(*CondRange, *CombinedCondition);
-
- for (std::size_t Index = 1; Index < Chain.size(); ++Index) {
- const IfStmt *const Nested = Chain[Index];
- Diag << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
- Nested->getBeginLoc(), Nested->getThen()->getBeginLoc()));
-
- if (const auto *Wrapper =
- dyn_cast<CompoundStmt>(Chain[Index - 1]->getThen())) {
- Diag << FixItHint::CreateRemoval(Wrapper->getLBracLoc())
- << FixItHint::CreateRemoval(Wrapper->getRBracLoc());
- }
- }
+ diagnoseChain(*this, If, Context, WarnOnDependentConstexprIf,
+ AllowUserDefinedBoolConversion);
}
} // namespace clang::tidy::readability
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
index 4e85017a70107..c8cabf2972e51 100644
--- a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.h
@@ -17,12 +17,6 @@ namespace clang::tidy::readability {
/// concatenating conditions with ``&&``.
class RedundantNestedIfCheck : public ClangTidyCheck {
public:
- enum class UserDefinedBoolConversionMode {
- None,
- WarnOnly,
- WarnAndFix,
- };
-
RedundantNestedIfCheck(StringRef Name, ClangTidyContext *Context);
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
@@ -31,7 +25,7 @@ class RedundantNestedIfCheck : public ClangTidyCheck {
private:
const bool WarnOnDependentConstexprIf;
- const UserDefinedBoolConversionMode UserBoolConversionMode;
+ const bool AllowUserDefinedBoolConversion;
};
} // namespace clang::tidy::readability
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
index 51c502114b55b..225098f2470d1 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/redundant-nested-if.rst
@@ -3,8 +3,8 @@
readability-redundant-nested-if
===============================
-Finds nested ``if`` statements that can be merged by combining their conditions
-with ``&&``.
+Finds nested ``if`` statements that can be merged by combining their
+conditions with ``&&``.
Example:
@@ -48,7 +48,7 @@ The check also supports outer declaration conditions in C++17 and later:
.. code-block:: c++
- if (bool x = ready()) {
+ if (bool X = ready()) {
if (can_run()) {
work();
}
@@ -58,7 +58,7 @@ becomes
.. code-block:: c++
- if (bool x = ready(); x && (can_run())) {
+ if (bool X = ready(); X && (can_run())) {
work();
}
@@ -73,14 +73,14 @@ The check only transforms chains where:
- In C++17 and later, the outermost ``if`` may use a condition variable if it
can be rewritten to an init-statement form, for example
- ``if (auto v = f())`` to ``if (auto v = f(); v && ...)``.
+ ``if (auto V = f())`` to ``if (auto V = f(); V && ...)``.
- When the outermost statement is already in ``if (init; cond)`` form, the
check keeps ``init`` unchanged and merges only into ``cond``.
- By default, merged conditions avoid user-defined ``bool`` conversions to
- preserve short-circuit semantics. This can be changed with
- :option:`UserDefinedBoolConversionMode`.
+ preserve built-in ``&&`` semantics. This can be changed with
+ :option:`AllowUserDefinedBoolConversion`.
- Only the outermost ``if`` may have an init-statement.
@@ -91,11 +91,11 @@ The check only transforms chains where:
- No merged ``if`` statement has statement attributes.
-- All rewritten ranges are free of macro/preprocessor-sensitive edits.
+- All rewritten ranges are free of macro- and preprocessor-sensitive edits.
- Fix-its are suppressed when comments in removed nested headers cannot be
- preserved safely. Comments inside conditions are preserved, while
- other comments between the ``ifs`` disable fix-its.
+ preserved safely. Comments inside conditions are preserved, while other
+ comments between the ``if`` statements disable fix-its.
For ``if constexpr``, nested merged conditions must be
non-instantiation-dependent to avoid template semantic changes. The outermost
@@ -105,24 +105,20 @@ constant ``true``.
Options
-------
-.. option:: UserDefinedBoolConversionMode
+.. option:: AllowUserDefinedBoolConversion
- Controls how chains with an outer condition that relies on user-defined
- ``bool`` conversion are handled.
+ When set to `true`, the check also merges chains whose conditions rely on
+ user-defined conversion to ``bool``.
- - `None`
- No diagnostic is emitted for those chains.
- - `WarnOnly`
- Emit diagnostics, but do not provide fix-its.
- - `WarnAndFix`
- Emit diagnostics and provide fix-its.
+ The fix-it inserts ``static_cast<bool>(...)`` where needed so the merged
+ condition still uses built-in ``&&`` semantics.
- Default is `None`.
+ Default is `false`.
.. option:: WarnOnDependentConstexprIf
When set to `true`, the check also emits diagnostics for remaining unsafe
- ``if constexpr`` chains (for example, with instantiation-dependent nested
- conditions), but does not provide a fix-it for them.
+ ``if constexpr`` chains, for example with instantiation-dependent nested
+ conditions, but does not provide a fix-it for them.
Default is `false`.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
index 7c26883395718..df4d74b84427d 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/readability/redundant-nested-if.cpp
@@ -2,26 +2,29 @@
// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17 -std=c++17 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20 -std=c++20 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23 -std=c++23 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
-// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26 -std=c++26 %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
-// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26,OPTWARN -std=c++26 %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.WarnOnDependentConstexprIf: true, readability-redundant-nested-if.UserDefinedBoolConversionMode: WarnOnly}}' -- -fno-delayed-template-parsing
-// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,OPTFIX -std=c++17 %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.UserDefinedBoolConversionMode: WarnAndFix}}' -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26 -std=c++26-or-later %s readability-redundant-nested-if %t -- -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26,DEPWARN -std=c++26-or-later %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.WarnOnDependentConstexprIf: true}}' -- -fno-delayed-template-parsing
+// RUN: %check_clang_tidy -check-suffixes=BASE,CXX17,CXX20,CXX23,CXX26,ALLOWBOOL -std=c++26-or-later %s readability-redundant-nested-if %t -- -config='{CheckOptions: {readability-redundant-nested-if.AllowUserDefinedBoolConversion: true}}' -- -fno-delayed-template-parsing
bool cond(int X = 0);
+int side_effect();
void sink();
void bar();
+
struct BoolLike {
explicit operator bool() const;
};
+
BoolLike make_bool_like();
#define INNER_IF(C) if (C) sink()
#define COND_MACRO cond()
#define OUTER_IF if (cond())
-// Core coverage under default options.
void positive_cases() {
// CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond(1)) {
sink();
}
@@ -29,22 +32,25 @@ void positive_cases() {
// CHECK-FIXES-BASE: if ((cond()) && (cond(1)))
// CHECK-FIXES-BASE: sink();
- // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (cond() || cond(1))
- if (cond(2))
- sink();
- // CHECK-FIXES-BASE: if ((cond() || cond(1)) && (cond(2)))
- // CHECK-FIXES-BASE: sink();
-
// CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) {
- if (cond(1))
- if (cond(2)) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
+ if (cond(1)) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:7: note: nested if statement to merge is here
+ if (cond(2))
sink();
- }
+ }
}
// CHECK-FIXES-BASE: if ((cond()) && (cond(1)) && (cond(2)))
// CHECK-FIXES-BASE: sink();
+
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ if (cond() || cond(1))
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
+ if (cond(2))
+ sink();
+ // CHECK-FIXES-BASE: if ((cond() || cond(1)) && (cond(2)))
+ // CHECK-FIXES-BASE: sink();
}
void stress_long_chain_case() {
@@ -78,12 +84,14 @@ void stress_long_chain_case() {
}
void nested_chains_are_diagnosed_once_per_chain() {
- // CHECK-MESSAGES-BASE: :[[@LINE+2]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-BASE: :[[@LINE+4]]:7: warning: nested if statements can be merged
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond(1)) {
sink();
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:7: warning: nested if statements can be merged
if (cond(2)) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:9: note: nested if statement to merge is here
if (cond(3))
sink();
}
@@ -93,10 +101,11 @@ void nested_chains_are_diagnosed_once_per_chain() {
// CHECK-FIXES-BASE: if ((cond(2)) && (cond(3)))
}
-void child_chain_is_reported_when_macro_parent_is_unfixable() {
+void child_chain_is_reported_when_parent_is_not_diagnosable() {
// CHECK-MESSAGES-BASE: :[[@LINE+2]]:5: warning: nested if statements can be merged
OUTER_IF {
if (cond(1)) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:7: note: nested if statement to merge is here
if (cond(2))
sink();
}
@@ -106,16 +115,23 @@ void child_chain_is_reported_when_macro_parent_is_unfixable() {
// CHECK-FIXES-BASE: sink();
}
-void negative_cases() {
- // CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (bool B = cond()) {
- if (cond(1))
- sink();
+void else_branch_child_chain_is_reported_when_parent_is_not_diagnosable() {
+ // CHECK-MESSAGES-BASE: :[[@LINE+4]]:5: warning: nested if statements can be merged
+ if (cond()) {
+ sink();
+ } else {
+ if (cond(1)) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:7: note: nested if statement to merge is here
+ if (cond(2))
+ sink();
+ }
}
- // CHECK-FIXES-CXX17: if (bool B = cond(); B && (cond(1)))
- // CHECK-FIXES-CXX17: sink();
+ // CHECK-FIXES-BASE: } else {
+ // CHECK-FIXES-BASE: if ((cond(1)) && (cond(2)))
+ // CHECK-FIXES-BASE: sink();
+}
+void negative_cases() {
// CHECK-MESSAGES-BASE-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) {
if (bool B = cond(1))
@@ -151,20 +167,6 @@ void negative_cases() {
sink();
sink();
}
-
-}
-
-// Option-specific behavior for UserDefinedBoolConversionMode.
-void user_defined_bool_conversion_mode_cases() {
- // CHECK-MESSAGES-BASE-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTWARN: :[[@LINE+2]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTFIX: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (make_bool_like()) {
- if (cond(1))
- sink();
- }
- // CHECK-FIXES-OPTFIX: if ((make_bool_like()) && (cond(1)))
- // CHECK-FIXES-OPTFIX: sink();
}
void macro_and_preprocessor_cases() {
@@ -192,37 +194,59 @@ void comment_handling_cases() {
// Comments inside condition payloads are preserved by the merged condition.
// CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond() /* outer payload */) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (/* inner payload */ cond(1))
sink();
}
// CHECK-FIXES-BASE: if ((cond() /* outer payload */) && (/* inner payload */ cond(1)))
// CHECK-FIXES-BASE: sink();
- // Trailing comments in nested headers are warning-only (no fix-it).
+ // Trailing comments in nested headers keep the diagnostic but suppress fix-its.
// CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) // outer trailing
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond(1)) // inner trailing
sink();
// CHECK-FIXES-BASE: if (cond()) // outer trailing
- // CHECK-FIXES-BASE-NEXT: if (cond(1)) // inner trailing
- // CHECK-FIXES-BASE-NEXT: sink();
+ // CHECK-FIXES-BASE: if (cond(1)) // inner trailing
+ // CHECK-FIXES-BASE: sink();
- // Comments in other nested-header locations are warning-only (no fix-it).
// CHECK-MESSAGES-BASE: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (cond()) {
+ // CHECK-MESSAGES-BASE: :[[@LINE+1]]:5: note: nested if statement to merge is here
if /* nested header comment */ (cond(1))
sink();
}
// CHECK-FIXES-BASE: if /* nested header comment */ (cond(1))
}
-#if __cplusplus >= 201703L
-int side_effect();
+void user_defined_bool_conversion_cases() {
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:5: note: nested if statement to merge is here
+ if (make_bool_like()) {
+ if (cond(1))
+ sink();
+ }
+ // CHECK-FIXES-ALLOWBOOL: if ((static_cast<bool>(make_bool_like())) && (cond(1)))
+ // CHECK-FIXES-ALLOWBOOL: sink();
-// C++17 language feature coverage.
+ // CHECK-MESSAGES-BASE-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:5: note: nested if statement to merge is here
+ if (cond(1)) {
+ if (make_bool_like())
+ sink();
+ }
+ // CHECK-FIXES-ALLOWBOOL: if ((cond(1)) && (static_cast<bool>(make_bool_like())))
+ // CHECK-FIXES-ALLOWBOOL: sink();
+}
+
+#if __cplusplus >= 201703L
void init_statement_cases() {
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (side_effect(); cond()) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond(1))
sink();
}
@@ -230,30 +254,19 @@ void init_statement_cases() {
// CHECK-FIXES-CXX17: sink();
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (auto Guard = side_effect()) {
+ if (bool B = cond()) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond(1))
sink();
}
- // CHECK-FIXES-CXX17: if (auto Guard = side_effect(); Guard && (cond(1)))
+ // CHECK-FIXES-CXX17: if (bool B = cond(); B && (cond(1)))
// CHECK-FIXES-CXX17: sink();
- // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (cond()) {
- if (bool InnerInit = cond(1); InnerInit)
- sink();
- }
-
- // Macro-expanded root conditions are diagnostic-only unsafe, so no warning
- // with fix-it is emitted.
- // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (side_effect(); COND_MACRO) {
- if (cond(1))
- sink();
- }
-
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (bool X = cond(); X) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (cond()) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:7: note: nested if statement to merge is here
if (cond(1))
bar();
}
@@ -263,43 +276,30 @@ void init_statement_cases() {
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (bool X = cond() /* here */) {
- if (cond()) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
+ if (cond())
bar();
- }
}
// CHECK-FIXES-CXX17: if (bool X = cond() /* here */; X && (cond()))
// CHECK-FIXES-CXX17: bar();
}
-void declaration_condition_type_safety() {
+void declaration_condition_boollike_cases() {
// CHECK-MESSAGES-CXX17-NOT: :[[@LINE+3]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTWARN: :[[@LINE+2]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTFIX: :[[@LINE+1]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:3: warning: nested if statements can be merged
+ // CHECK-MESSAGES-ALLOWBOOL: :[[@LINE+2]]:5: note: nested if statement to merge is here
if (auto Guard = make_bool_like()) {
if (cond(1))
sink();
}
- // CHECK-FIXES-OPTFIX: if (auto Guard = make_bool_like(); Guard && (cond(1)))
- // CHECK-FIXES-OPTFIX: sink();
+ // CHECK-FIXES-ALLOWBOOL: if (auto Guard = make_bool_like(); static_cast<bool>(Guard) && (cond(1)))
+ // CHECK-FIXES-ALLOWBOOL: sink();
- // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
- if (bool X = cond()) {
- if (cond()) {
- if (cond(1))
- bar();
- }
- }
- // CHECK-FIXES-CXX17: if (bool X = cond(); X && (cond()) && (cond(1)))
- // CHECK-FIXES-CXX17: bar();
-
- // Macro-expanded declaration conditions are diagnostic-only unsafe, so no
- // warning with fix-it is emitted.
// CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (bool X = COND_MACRO) {
if (cond(1))
sink();
}
-
}
constexpr bool C1 = true;
@@ -308,9 +308,9 @@ constexpr bool C2 = false;
void constexpr_non_template() {
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (C1) {
- if constexpr (C2) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
+ if constexpr (C2)
sink();
- }
}
// CHECK-FIXES-CXX17: if constexpr ((C1) && (C2))
// CHECK-FIXES-CXX17: sink();
@@ -319,6 +319,7 @@ void constexpr_non_template() {
template <bool B> void dependent_constexpr_outer_is_fixable() {
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (B) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (true)
sink();
}
@@ -327,18 +328,18 @@ template <bool B> void dependent_constexpr_outer_is_fixable() {
}
template <bool B> void dependent_constexpr_outer_is_unsafe_when_nested_false() {
- // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
if constexpr (B) {
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (false)
sink();
}
}
template <typename T> void dependent_constexpr_operand_warn_only_under_option() {
- // CHECK-MESSAGES-CXX17-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
if constexpr (true) {
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (sizeof(T) == 4)
sink();
}
@@ -347,6 +348,7 @@ template <typename T> void dependent_constexpr_operand_warn_only_under_option()
template <typename T> void dependent_constexpr_type_chain_outer_is_fixable() {
// CHECK-MESSAGES-CXX17: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (sizeof(T) == 4) {
+ // CHECK-MESSAGES-CXX17: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (true)
sink();
}
@@ -371,8 +373,9 @@ void mixed_constexpr_and_non_constexpr(bool B) {
#if __cplusplus >= 202400L
template <typename T> void constexpr_template_static_assert() {
// CHECK-MESSAGES-CXX26-NOT: :[[@LINE+2]]:3: warning: nested if statements can be merged
- // CHECK-MESSAGES-OPTWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:3: warning: nested instantiation-dependent if constexpr statements can be merged
if constexpr (sizeof(T) == 1) {
+ // CHECK-MESSAGES-DEPWARN: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (false) {
static_assert(false, "discarded in template context");
}
@@ -386,6 +389,7 @@ template <typename T> void constexpr_template_static_assert() {
void constexpr_requires_expression_cases() {
// CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (requires { sizeof(int); }) {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (requires { sizeof(long); })
sink();
}
@@ -396,6 +400,7 @@ void constexpr_requires_expression_cases() {
void constexpr_init_statement_cases() {
// CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (constexpr bool HasInt = requires { sizeof(int); }; HasInt) {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (requires { sizeof(long); })
sink();
}
@@ -406,6 +411,7 @@ void constexpr_init_statement_cases() {
template <typename T> void dependent_requires_outer_is_fixable() {
// CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
if constexpr (requires { typename T::type; }) {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:5: note: nested if statement to merge is here
if constexpr (true)
sink();
}
@@ -426,11 +432,10 @@ void attribute_cases(bool B1, bool B2) {
if (B2)
sink();
}
-}
-void still_merges_without_attributes(bool B1, bool B2) {
// CHECK-MESSAGES-CXX20: :[[@LINE+1]]:3: warning: nested if statements can be merged
if (B1) {
+ // CHECK-MESSAGES-CXX20: :[[@LINE+1]]:5: note: nested if statement to merge is here
if (B2)
sink();
}
@@ -440,8 +445,6 @@ void still_merges_without_attributes(bool B1, bool B2) {
#endif
#ifdef __cpp_if_consteval
-consteval bool compile_time_true() { return true; }
-
void consteval_cases(bool B1, bool B2) {
// CHECK-MESSAGES-CXX23-NOT: :[[@LINE+1]]:3: warning: nested if statements can be merged
if consteval {
@@ -460,7 +463,6 @@ void consteval_cases(bool B1, bool B2) {
sink();
}
}
-
}
template <bool B> constexpr void consteval_nested_in_constexpr() {
>From aa7dbfc365f960dff74bc0c3cd0481cc5fd36fd7 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 00:05:22 +0300
Subject: [PATCH 5/5] Fix format
---
.../clang-tidy/readability/RedundantNestedIfCheck.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
index 371c5cf31b779..57e31df0f530a 100644
--- a/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
+++ b/clang-tools-extra/clang-tidy/readability/RedundantNestedIfCheck.cpp
@@ -451,9 +451,8 @@ buildCombinedCondition(llvm::ArrayRef<const IfStmt *> Chain,
for (const auto &[Index, If] : llvm::enumerate(Chain)) {
const bool IsRoot = Index == 0;
- if (!IsRoot && !hasOnlyPayloadCommentsInNestedHeader(If, SM, LangOpts)) {
+ if (!IsRoot && !hasOnlyPayloadCommentsInNestedHeader(If, SM, LangOpts))
return {CombinedConditionBuildStatus::UnsupportedCommentPlacement, {}};
- }
if (IsRoot && If->hasVarStorage()) {
const auto *const ConditionVariable = If->getConditionVariable();
More information about the cfe-commits
mailing list