Skip to content

Commit

Permalink
[clang-tidy] Add bugprone-chained-comparison check (#76365)
Browse files Browse the repository at this point in the history
Check that flags chained comparison expressions,
such as a < b < c or a == b == c, which may have
unintended behavior due to implicit operator
associativity.

Moved from Phabricator  (D144429).
  • Loading branch information
PiotrZSL committed Jan 22, 2024
1 parent 27eb8d5 commit 06c3c3b
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 0 deletions.
3 changes: 3 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
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
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
@@ -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
@@ -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
Expand Up @@ -143,6 +143,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
@@ -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
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
@@ -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;
}

0 comments on commit 06c3c3b

Please sign in to comment.