Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[clang-tidy] Add bugprone-chained-comparison check #76365

Merged
merged 7 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "BoolPointerImplicitConversionCheck.h"
#include "BranchCloneCheck.h"
#include "CastingThroughVoidCheck.h"
#include "ChainedComparisonCheck.h"
#include "ComparePointerToMemberVirtualFunctionCheck.h"
#include "CopyConstructorInitCheck.h"
#include "DanglingHandleCheck.h"
Expand Down Expand Up @@ -108,6 +109,8 @@ class BugproneModule : public ClangTidyModule {
CheckFactories.registerCheck<BranchCloneCheck>("bugprone-branch-clone");
CheckFactories.registerCheck<CastingThroughVoidCheck>(
"bugprone-casting-through-void");
CheckFactories.registerCheck<ChainedComparisonCheck>(
"bugprone-chained-comparison");
CheckFactories.registerCheck<ComparePointerToMemberVirtualFunctionCheck>(
"bugprone-compare-pointer-to-member-virtual-function");
CheckFactories.registerCheck<CopyConstructorInitCheck>(
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_clang_library(clangTidyBugproneModule
BranchCloneCheck.cpp
BugproneTidyModule.cpp
CastingThroughVoidCheck.cpp
ChainedComparisonCheck.cpp
ComparePointerToMemberVirtualFunctionCheck.cpp
CopyConstructorInitCheck.cpp
DanglingHandleCheck.cpp
Expand Down
150 changes: 150 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/ChainedComparisonCheck.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//===--- ChainedComparisonCheck.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 "ChainedComparisonCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include <algorithm>

using namespace clang::ast_matchers;

namespace clang::tidy::bugprone {
static bool isExprAComparisonOperator(const Expr *E) {
if (const auto *Op = dyn_cast_or_null<BinaryOperator>(E->IgnoreImplicit()))
return Op->isComparisonOp();
if (const auto *Op =
dyn_cast_or_null<CXXOperatorCallExpr>(E->IgnoreImplicit()))
return Op->isComparisonOp();
return false;
}

namespace {
AST_MATCHER(BinaryOperator,
hasBinaryOperatorAChildComparisonOperatorWithoutParen) {
return isExprAComparisonOperator(Node.getLHS()) ||
isExprAComparisonOperator(Node.getRHS());
}

AST_MATCHER(CXXOperatorCallExpr,
hasCppOperatorAChildComparisonOperatorWithoutParen) {
return std::any_of(Node.arg_begin(), Node.arg_end(),
isExprAComparisonOperator);
}

struct ChainedComparisonData {
llvm::SmallString<256U> Name;
llvm::SmallVector<const Expr *, 32U> Operands;

explicit ChainedComparisonData(const Expr *Op) { extract(Op); }

private:
void add(const Expr *Operand);
void add(llvm::StringRef Opcode);
void extract(const Expr *Op);
void extract(const BinaryOperator *Op);
void extract(const CXXOperatorCallExpr *Op);
};

void ChainedComparisonData::add(const Expr *Operand) {
if (!Name.empty())
Name += ' ';
Name += 'v';
Name += std::to_string(Operands.size());
Operands.push_back(Operand);
}

void ChainedComparisonData::add(llvm::StringRef Opcode) {
Name += ' ';
Name += Opcode;
}

void ChainedComparisonData::extract(const BinaryOperator *Op) {
const Expr *LHS = Op->getLHS()->IgnoreImplicit();
if (isExprAComparisonOperator(LHS))
extract(LHS);
else
add(LHS);

add(Op->getOpcodeStr());

const Expr *RHS = Op->getRHS()->IgnoreImplicit();
if (isExprAComparisonOperator(RHS))
extract(RHS);
else
add(RHS);
}

void ChainedComparisonData::extract(const CXXOperatorCallExpr *Op) {
const Expr *FirstArg = Op->getArg(0U)->IgnoreImplicit();
if (isExprAComparisonOperator(FirstArg))
extract(FirstArg);
else
add(FirstArg);

add(getOperatorSpelling(Op->getOperator()));

const Expr *SecondArg = Op->getArg(1U)->IgnoreImplicit();
if (isExprAComparisonOperator(SecondArg))
extract(SecondArg);
else
add(SecondArg);
}

void ChainedComparisonData::extract(const Expr *Op) {
if (!Op)
return;

if (const auto *BinaryOp = dyn_cast<BinaryOperator>(Op)) {
extract(BinaryOp);
return;
}

if (const auto *OverloadedOp = dyn_cast<CXXOperatorCallExpr>(Op)) {
if (OverloadedOp->getNumArgs() == 2U)
extract(OverloadedOp);
}
}

} // namespace

void ChainedComparisonCheck::registerMatchers(MatchFinder *Finder) {
const auto OperatorMatcher = expr(anyOf(
binaryOperator(isComparisonOperator(),
hasBinaryOperatorAChildComparisonOperatorWithoutParen()),
cxxOperatorCallExpr(
isComparisonOperator(),
hasCppOperatorAChildComparisonOperatorWithoutParen())));

Finder->addMatcher(
expr(OperatorMatcher, unless(hasParent(OperatorMatcher))).bind("op"),
this);
}

void ChainedComparisonCheck::check(const MatchFinder::MatchResult &Result) {
const auto *MatchedOperator = Result.Nodes.getNodeAs<Expr>("op");

ChainedComparisonData Data(MatchedOperator);
if (Data.Operands.empty())
return;

diag(MatchedOperator->getBeginLoc(),
"chained comparison '%0' may generate unintended results, use "
"parentheses to specify order of evaluation or a logical operator to "
"separate comparison expressions")
<< llvm::StringRef(Data.Name).trim() << MatchedOperator->getSourceRange();

for (std::size_t Index = 0U; Index < Data.Operands.size(); ++Index) {
diag(Data.Operands[Index]->getBeginLoc(), "operand 'v%0' is here",
DiagnosticIDs::Note)
<< Index << Data.Operands[Index]->getSourceRange();
}
}

} // namespace clang::tidy::bugprone
34 changes: 34 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/ChainedComparisonCheck.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//===--- ChainedComparisonCheck.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_BUGPRONE_CHAINEDCOMPARISONCHECK_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_CHAINEDCOMPARISONCHECK_H

#include "../ClangTidyCheck.h"

namespace clang::tidy::bugprone {

/// Check detects chained comparison operators that can lead to unintended
/// behavior or logical errors.
///
/// For the user-facing documentation see:
/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/chained-comparison.html
class ChainedComparisonCheck : public ClangTidyCheck {
public:
ChainedComparisonCheck(StringRef Name, ClangTidyContext *Context)
: ClangTidyCheck(Name, Context) {}
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
std::optional<TraversalKind> getCheckTraversalKind() const override {
return TK_IgnoreUnlessSpelledInSource;
}
};

} // namespace clang::tidy::bugprone

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_CHAINEDCOMPARISONCHECK_H
6 changes: 6 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ New checks

Detects unsafe or redundant two-step casting operations involving ``void*``.

- New :doc:`bugprone-chained-comparison
<clang-tidy/checks/bugprone/chained-comparison>` check.

Check detects chained comparison operators that can lead to unintended
behavior or logical errors.

- New :doc:`bugprone-compare-pointer-to-member-virtual-function
<clang-tidy/checks/bugprone/compare-pointer-to-member-virtual-function>` check.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.. title:: clang-tidy - bugprone-chained-comparison

bugprone-chained-comparison
===========================

Check detects chained comparison operators that can lead to unintended
behavior or logical errors.

Chained comparisons are expressions that use multiple comparison operators
to compare three or more values. For example, the expression ``a < b < c``
compares the values of ``a``, ``b``, and ``c``. However, this expression does
not evaluate as ``(a < b) && (b < c)``, which is probably what the developer
intended. Instead, it evaluates as ``(a < b) < c``, which may produce
unintended results, especially when the types of ``a``, ``b``, and ``c`` are
different.

To avoid such errors, the check will issue a warning when a chained
comparison operator is detected, suggesting to use parentheses to specify
the order of evaluation or to use a logical operator to separate comparison
expressions.

Consider the following examples:

.. code-block:: c++

int a = 2, b = 6, c = 4;
if (a < b < c) {
// This block will be executed
}


In this example, the developer intended to check if ``a`` is less than ``b``
and ``b`` is less than ``c``. However, the expression ``a < b < c`` is
equivalent to ``(a < b) < c``. Since ``a < b`` is ``true``, the expression
``(a < b) < c`` is evaluated as ``1 < c``, which is equivalent to ``true < c``
and is invalid in this case as ``b < c`` is ``false``.

Even that above issue could be detected as comparison of ``int`` to ``bool``,
there is more dangerous example:

.. code-block:: c++

bool a = false, b = false, c = true;
if (a == b == c) {
// This block will be executed
}

In this example, the developer intended to check if ``a``, ``b``, and ``c`` are
all equal. However, the expression ``a == b == c`` is evaluated as
``(a == b) == c``. Since ``a == b`` is true, the expression ``(a == b) == c``
is evaluated as ``true == c``, which is equivalent to ``true == true``.
This comparison yields ``true``, even though ``a`` and ``b`` are ``false``, and
are not equal to ``c``.

To avoid this issue, the developer can use a logical operator to separate the
comparison expressions, like this:

.. code-block:: c++

if (a == b && b == c) {
// This block will not be executed
}


Alternatively, use of parentheses in the comparison expressions can make the
developer's intention more explicit and help avoid misunderstanding.

.. code-block:: c++

if ((a == b) == c) {
// This block will be executed
}

1 change: 1 addition & 0 deletions clang-tools-extra/docs/clang-tidy/checks/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Clang-Tidy Checks
:doc:`bugprone-bool-pointer-implicit-conversion <bugprone/bool-pointer-implicit-conversion>`, "Yes"
:doc:`bugprone-branch-clone <bugprone/branch-clone>`,
:doc:`bugprone-casting-through-void <bugprone/casting-through-void>`,
:doc:`bugprone-chained-comparison <bugprone/chained-comparison>`,
:doc:`bugprone-compare-pointer-to-member-virtual-function <bugprone/compare-pointer-to-member-virtual-function>`,
:doc:`bugprone-copy-constructor-init <bugprone/copy-constructor-init>`, "Yes"
:doc:`bugprone-dangling-handle <bugprone/dangling-handle>`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// RUN: %check_clang_tidy %s bugprone-chained-comparison %t

void badly_chained_1(int x, int y, int z)
{
int result = x < y < z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 < v1 < v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_2(int x, int y, int z)
{
int result = x <= y <= z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 <= v1 <= v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_3(int x, int y, int z)
{
int result = x > y > z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 > v1 > v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_4(int x, int y, int z)
{
int result = x >= y >= z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 >= v1 >= v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_5(int x, int y, int z)
{
int result = x == y != z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 == v1 != v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_6(int x, int y, int z)
{
int result = x != y == z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 != v1 == v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_multiple(int a, int b, int c, int d, int e, int f, int g, int h)
{
int result = a == b == c == d == e == f == g == h;
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 == v1 == v2 == v3 == v4 == v5 == v6 == v7' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_limit(int v[29])
{
// CHECK-MESSAGES: :[[@LINE+1]]:18: warning: chained comparison 'v0 == v1 == v2 == v3 == v4 == v5 == v6 == v7 == v8 == v9 == v10 == v11 == v12 == v13 == v14 == v15 == v16 == v17 == v18 == v19 == v20 == v21 == v22 == v23 == v24 == v25 == v26 == v27 == v28' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]
int result = v[0] == v[1] == v[2] == v[3] == v[4] == v[5] == v[6] == v[7] ==
v[8] == v[9] == v[10] == v[11] == v[12] == v[13] == v[14] ==
v[15] == v[16] == v[17] == v[18] == v[19] == v[20] == v[21] ==
v[22] == v[23] == v[24] == v[25] == v[26] == v[27] == v[28];

}

void badly_chained_parens2(int x, int y, int z, int t, int a, int b)
{
int result = (x < y) < (z && t) > (a == b);
}
// CHECK-MESSAGES: :[[@LINE-2]]:18: warning: chained comparison 'v0 < v1 > v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void badly_chained_inner(int x, int y, int z, int t, int u)
{
int result = x && y < z < t && u;
}
// CHECK-MESSAGES: :[[@LINE-2]]:23: warning: chained comparison 'v0 < v1 < v2' may generate unintended results, use parentheses to specify order of evaluation or a logical operator to separate comparison expressions [bugprone-chained-comparison]

void properly_chained_1(int x, int y, int z)
{
int result = x < y && y < z;
}

void properly_chained_2(int x, int y, int z)
{
int result = (x < y) == z;
}

Loading