diff --git a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt --- a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt @@ -48,6 +48,7 @@ UniqueptrDeleteReleaseCheck.cpp UppercaseLiteralSuffixCheck.cpp UseAnyOfAllOfCheck.cpp + UseEarlyExitsCheck.cpp LINK_LIBS clangTidy diff --git a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp --- a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp @@ -51,6 +51,7 @@ #include "UniqueptrDeleteReleaseCheck.h" #include "UppercaseLiteralSuffixCheck.h" #include "UseAnyOfAllOfCheck.h" +#include "UseEarlyExitsCheck.h" namespace clang { namespace tidy { @@ -143,6 +144,8 @@ "readability-uppercase-literal-suffix"); CheckFactories.registerCheck( "readability-use-anyofallof"); + CheckFactories.registerCheck( + "readability-use-early-exits"); } }; diff --git a/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.h b/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.h @@ -0,0 +1,42 @@ +//===--- UseEarlyExitsCheck.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_USEEARLYEXITSCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USEEARLYEXITSCHECK_H + +#include "../ClangTidyCheck.h" + +namespace clang { +namespace tidy { +namespace readability { + +/// Finds `if` statements inside functions and loops that could be flipped to +/// make an early exit. +/// +/// For the user-facing documentation see: +/// http://clang.llvm.org/extra/clang-tidy/checks/readability/use-early-exits.html +class UseEarlyExitsCheck : public ClangTidyCheck { +public: + UseEarlyExitsCheck(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; + + unsigned getLineCountThreshold() const { return LineCountThreshold; } + bool getWrapInBraces() const { return WrapInBraces; } + +private: + const unsigned LineCountThreshold; + const bool WrapInBraces; +}; + +} // namespace readability +} // namespace tidy +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USEEARLYEXITSCHECK_H diff --git a/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.cpp b/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/readability/UseEarlyExitsCheck.cpp @@ -0,0 +1,259 @@ +//===--- UseEarlyExitsCheck.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 "UseEarlyExitsCheck.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Lex/Lexer.h" + +using namespace clang::ast_matchers; + +namespace clang { +namespace tidy { +namespace readability { + +static bool needsParensAfterUnaryNegation(const Expr *E) { + E = E->IgnoreImpCasts(); + if (isa(E) || isa(E)) + return true; + + if (const auto *Op = dyn_cast(E)) + return Op->getNumArgs() == 2 && Op->getOperator() != OO_Call && + Op->getOperator() != OO_Subscript; + + return false; +} + +static std::vector createReverseConditionFix(const Expr *Condition, + const ASTContext &Ctx) { + if (const auto *BO = dyn_cast(Condition)) { + switch (BO->getOpcode()) { + case BO_EQ: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), "!=")}; + case BO_NE: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), "==")}; + case BO_LT: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), ">=")}; + case BO_LE: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), ">")}; + case BO_GT: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), "<=")}; + case BO_GE: + return {FixItHint::CreateReplacement(BO->getOperatorLoc(), "<")}; + default: + break; + } + } else if (const auto *UO = dyn_cast(Condition)) { + if (UO->getOpcode() == UO_LNot) + return {FixItHint::CreateRemoval(UO->getOperatorLoc())}; + } else if (const auto *BL = dyn_cast(Condition)) { + return {FixItHint::CreateReplacement(BL->getSourceRange(), + BL->getValue() ? "false" : "true")}; + } else if (const auto *IL = dyn_cast(Condition)) { + return {FixItHint::CreateReplacement( + IL->getSourceRange(), IL->getValue().getBoolValue() ? "0" : "1")}; + } + std::vector FixIts; + if (needsParensAfterUnaryNegation(Condition)) { + FixIts.push_back( + FixItHint::CreateInsertion(Condition->getBeginLoc(), "!(")); + FixIts.push_back(FixItHint::CreateInsertion( + Lexer::getLocForEndOfToken(Condition->getEndLoc(), 0, + Ctx.getSourceManager(), Ctx.getLangOpts()), + ")")); + } else { + FixIts.push_back(FixItHint::CreateInsertion(Condition->getBeginLoc(), "!")); + } + return FixIts; +} + +static llvm::iterator_range +getBodyReverse(const CompoundStmt *S) { + return {S->body_rbegin(), S->body_rend()}; +} + +namespace { + +class EarlyExitVisitor : public RecursiveASTVisitor { + UseEarlyExitsCheck &Check; + ASTContext &Ctx; + +public: + EarlyExitVisitor(UseEarlyExitsCheck &Check, ASTContext &Ctx) + : Check(Check), Ctx(Ctx) {} + + bool traverse() { return TraverseAST(Ctx); } + + template + void diagnoseIf(const IfStmt *If, StringRef Continuation); + + template + void checkBody(const CompoundStmt *CS, StringRef Continuation) { + for (const Stmt *Item : getBodyReverse(CS)) { + // If the last statement in the function is a return, ignore it. + if (isa(Item)) + continue; + // While were here, we can safely ignore empty null stmts. + if (isa(Item) && !cast(Item)->hasLeadingEmptyMacro()) + continue; + if (const auto *If = dyn_cast(Item)) + diagnoseIf(If, Continuation); + break; + } + } + + bool VisitFunctionDecl(const FunctionDecl *Func) { + // Skip any declarations. + if (!Func->doesThisDeclarationHaveABody()) + return true; + // Just ignore no return functions. + if (Func->isNoReturn()) + return true; + // Early exit logic only works for functions that return void. + // FIXME: Explore options where following the IfStmt there is a return + // value with a literal return. + // bool hasSomething(Class *S) { + // if (S) { + // return S->hasSomething(); + // } + // return false; + // } + // bool hasSomething(Class *S) { + // if (!S) + // return false; + // return S->hasSomething(); + // return false; // Ideally this would be removed too. + // } + if (!Func->getReturnType()->isVoidType()) + return true; + // FIXME: explore if CoreturnStmt could work here also. + checkBody(cast(Func->getBody()), "return"); + return true; + } + + bool VisitForStmt(const ForStmt *For) { + if (const auto *CS = dyn_cast_or_null(For->getBody())) { + checkBody(CS, "continue"); + } + return true; + } + + bool VisitWhileStmt(const WhileStmt *While) { + if (const auto *CS = dyn_cast_or_null(While->getBody())) { + checkBody(CS, "continue"); + } + return true; + } + + bool VisitDoStmt(const DoStmt *Do) { + if (const auto *CS = dyn_cast_or_null(Do->getBody())) { + checkBody(CS, "continue"); + } + return true; + } + + bool VisitCXXForRangeStmt(const CXXForRangeStmt *ForRange) { + if (const auto *CS = dyn_cast_or_null(ForRange->getBody())) { + checkBody(CS, "continue"); + } + return true; + } +}; + +template +void EarlyExitVisitor::diagnoseIf(const IfStmt *If, StringRef Continuation) { + // Ignore const(expr|eval) if statements. + if (If->isConsteval() || If->isConstexpr()) + return; + // We can't transform if there is an else. + if (If->hasElseStorage()) + return; + // If there is a variable in the condition, This transformation would mean it + // goes out of scope before the current then branch can use it. + if (If->hasVarStorage()) + return; + // As above, only technically if the init doesn't actually have any decls, we + // could still do the transformation. But thats not exactly a common idiom. + if (If->hasInitStorage()) + return; + const auto *BodyCS = llvm::dyn_cast_or_null(If->getThen()); + if (!BodyCS) + return; + const SourceManager &SM = Ctx.getSourceManager(); + SourceLocation Begin = If->getBeginLoc(); + SourceLocation End = If->getEndLoc(); + FileID BeginFile = SM.getFileID(Begin); + FileID EndFile = SM.getFileID(End); + if (BeginFile != EndFile) + return; + unsigned BeginLine = SM.getSpellingLineNumber(Begin); + unsigned EndLine = SM.getSpellingLineNumber(End); + if (BeginLine > EndLine) + return; // This case can't be good. + if ((EndLine - BeginLine) < Check.getLineCountThreshold()) + return; + Check.diag(If->getBeginLoc(), "use early exit") + << createReverseConditionFix(If->getCond(), Ctx) + << FixItHint::CreateInsertion(If->getThen()->getBeginLoc(), + ((Check.getWrapInBraces() ? "{\n" : "\n") + + Continuation + + (Check.getWrapInBraces() ? ";\n}" : ";\n")) + .str()) + << FixItHint::CreateRemoval(BodyCS->getLBracLoc()) + << FixItHint::CreateRemoval(BodyCS->getRBracLoc()); + checkBody(BodyCS, Continuation); +} +} // namespace + +static bool fallbackBracesCheckActive(llvm::Optional Cur, + ClangTidyContext *Context) { + if (Cur) + return *Cur; + char Buff[64]; // Should only ever need 63 chars here. + constexpr llvm::StringLiteral OptName = ".ShortStatementLines"; + constexpr llvm::StringLiteral CheckAliases[] = { + "google-readability-braces-around-statements", + "readability-braces-around-statements", "hicpp-braces-around-statements"}; + for (StringRef CheckAlias : CheckAliases) { + if (!Context->isCheckEnabled(CheckAlias)) + continue; + memcpy(Buff, CheckAlias.data(), CheckAlias.size()); + memcpy(Buff + CheckAlias.size(), OptName.data(), OptName.size()); + auto Iter = Context->getOptions().CheckOptions.find( + {Buff, CheckAlias.size() + OptName.size()}); + if (Iter == Context->getOptions().CheckOptions.end()) + return true; + unsigned Value; + if (StringRef(Iter->getValue().Value).getAsInteger(10, Value) || Value < 2) + return true; + } + return false; +} + +UseEarlyExitsCheck::UseEarlyExitsCheck(StringRef Name, + ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + LineCountThreshold(Options.get("LineCountThreshold", 10U)), + WrapInBraces(fallbackBracesCheckActive(Options.get("WrapInBraces"), + Context)) {} + +void UseEarlyExitsCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "LineCountThreshold", LineCountThreshold); + Options.store(Opts, "WrapInBraces", WrapInBraces); +} + +void UseEarlyExitsCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher(translationUnitDecl(), this); +} + +void UseEarlyExitsCheck::check(const MatchFinder::MatchResult &Result) { + EarlyExitVisitor(*this, *Result.Context).traverse(); +} +} // namespace readability +} // namespace tidy +} // namespace clang diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -150,6 +150,12 @@ Future libc++ will remove the extension (`D120996 `). +- New :doc:`readability-use-early-exits + ` check. + + Finds ``if`` statements inside functions and loops that could be flipped to + make an early exit. + New check aliases ^^^^^^^^^^^^^^^^^ diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -359,6 +359,7 @@ `readability-uniqueptr-delete-release `_, "Yes" `readability-uppercase-literal-suffix `_, "Yes" `readability-use-anyofallof `_, + `readability-use-early-exits _` "Yes" `zircon-temporary-objects `_, diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/use-early-exits.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/use-early-exits.rst new file mode 100644 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/readability/use-early-exits.rst @@ -0,0 +1,77 @@ +.. title:: clang-tidy - readability-use-early-exits + +readability-use-early-exits +=========================== + +Finds ``if`` statements inside functions and loops that could be flipped to +make an early exit. + +Example: + +.. code-block:: c++ + + void Process(MyClass* C) { + if (C) { + // Do some long processing. + } + } + + void Process(span C){ + for (MyClass *Item : C) { + if (Item) { + // Do Some long processing. + } + } + } + +Transformed to: + +.. code-block:: c++ + + void Process(MyClass* C) { + if (!C) + return; + // Do some long processing. + } + + void Process(span C){ + for (MyClass *Item : C) { + if (!Item) + continue; + // Do Some long processing. + } + } + +Options +------- + +.. option:: LineCountThreshold + + Don't transform any ``if`` statement if the statement uses less than this + many lines. Default value is `10`. + +.. option:: WrapInBraces + + If `true`, the early exit will be wrapped in braces. + + If unspecified, the value will be inferred based on whether + :doc:`readability-braces-around-statements ` or one + of its alias' is active. + + .. code-block:: c++ + + void Process(MyClass *C) { + if (!C) { + return; + } + // Do some long processing + } + + void Process(span C){ + for (MyClass *Item : C) { + if (!Item) { + continue; + } + // Do Some long processing. + } + } diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits-braces.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits-braces.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits-braces.cpp @@ -0,0 +1,16 @@ +// RUN: %check_clang_tidy %s readability-use-early-exits,google-readability-braces-around-statements -format-style=llvm %t -- \ +// RUN: -config="{CheckOptions: {readability-use-early-exits.LineCountThreshold: 2, \ +// RUN: google-readability-braces-around-statements.ShortStatementLines: 0}}" + +// RUN: %check_clang_tidy %s readability-use-early-exits,hicpp-braces-around-statements -format-style=llvm %t -- \ +// RUN: -config="{CheckOptions: {readability-use-early-exits.LineCountThreshold: 2}}" +void nomralFunc(int *A) { + // CHECK-MESSAGES: [[@LINE+1]]:3: warning: use early exit + if (A) { + *A = 10; + } + // CHECK-FIXES: if (!A) { + // CHECK-FIXES-NEXT: return; + // CHECK-FIXES-NEXT: } + // CHECK-FIXES-NEXT: *A = 10; +} diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/readability/use-early-exits.cpp @@ -0,0 +1,164 @@ +// RUN: %check_clang_tidy %s readability-use-early-exits -format-style=llvm %t -- \ +// RUN: -config="{CheckOptions: {readability-use-early-exits.LineCountThreshold: 2}}" + +// Run the check with the braces around statements check also enabled, but +// ShortStatementLines is set so that we shouldn't add braces. +// RUN: %check_clang_tidy %s readability-use-early-exits,readability-braces-around-statements -format-style=llvm %t -- \ +// RUN: -config="{CheckOptions: {readability-use-early-exits.LineCountThreshold: 2, \ +// RUN: readability-braces-around-statements.ShortStatementLines: 2}}" + +// Just to hit the threshold +void padLines(); + +void nomralFunc(int *A) { + // CHECK-MESSAGES: [[@LINE+1]]:3: warning: use early exit + if (A) { + *A = 10; + } + // CHECK-FIXES: if (!A) + // CHECK-FIXES-NEXT: return; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: *A = 10; +} + +void funcWithTrailing(int *B) { + // CHECK-MESSAGES: [[@LINE+1]]:3: warning: use early exit + if (B) { + *B = 10; + } + return; + ; + // CHECK-FIXES: if (!B) + // CHECK-FIXES-NEXT: return; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: *B = 10; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: return; + // CHECK-FIXES-NEXT: ; +} + +void normal(int A, int B) { + + while (true) { // CHECK-MESSAGES: [[@LINE+1]]:5: warning: use early exit + if (A == B) { + padLines(); + } + } + // CHECK-FIXES: while (true) { + // CHECK-FIXES-NEXT: if (A != B) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: padLines(); + // CHECK-FIXES-NEXT: } + + // Eat continue and empty nulls + while (true) { // CHECK-MESSAGES: [[@LINE+1]]:5: warning: use early exit + if (A != B) { + padLines(); + } + continue; + ; + continue; + } + // CHECK-FIXES: while (true) { + // CHECK-FIXES-NEXT: if (A == B) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: padLines(); + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-NEXT: ; + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-NEXT: } +} + +void toShort(int A, int B) { + while (true) { + if (A == B) { + } + } +} + +void hasElse(int A, int B) { + while (true) { + if (A == B) { + + } else { + } + } +} + +void hasTrailingStmt(int A, int B) { + while (true) { + if (A == B) { + } + padLines(); + } +} + +void nested(int A, int B) { + // if (A > B) { + // CHECK-MESSAGES: [[@LINE+6]]:5: warning: use early exit + // if (B < A) { + // CHECK-MESSAGES: [[@LINE+5]]:7: warning: use early exit + // if (A == B) { + // CHECK-MESSAGES: [[@LINE+4]]:9: warning: use early exit + while (true) { // Nested + if (A > B) { + if (B < A) { + if (A != B) { + padLines(); + } + } + } + } // EndLoop + // CHECK-FIXES: while (true) { // Nested + // CHECK-FIXES-NEXT: if (A <= B) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: if (B >= A) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: if (A == B) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: padLines(); + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: } // EndLoop +} + +void badNested(bool B) { + // Ensure check doesn't check for nested `if` if outer `if` has else. + while (true) { + if (B) { + if (B) { + } + } else { + } + } +} + +void semiNested(int A, int B) { + // CHECK-MESSAGES: [[@LINE+2]]:5: warning: use early exit + while (true) { // SemiNested + if (A > B) { + if (B < A) { + if (A != B) { + padLines(); + } + } else { + } + } + } + // CHECK-FIXES: while (true) { // SemiNested + // CHECK-FIXES-NEXT: if (A <= B) + // CHECK-FIXES-NEXT: continue; + // CHECK-FIXES-EMPTY: + // CHECK-FIXES-NEXT: if (B < A) { + // CHECK-FIXES-NEXT: if (A != B) { + // CHECK-FIXES-NEXT: padLines(); + // CHECK-FIXES-NEXT: } + // CHECK-FIXES-NEXT: } else { + // CHECK-FIXES-NEXT: } + // CHECK-FIXES-NEXT: } +}