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

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).
This commit is contained in:
Piotr Zegar
2024-01-22 17:11:02 +01:00
committed by GitHub
parent 27eb8d53ae
commit 06c3c3b67c
9 changed files with 435 additions and 0 deletions

View File

@@ -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"
@@ -108,6 +109,8 @@ public:
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>(

View File

@@ -12,6 +12,7 @@ add_clang_library(clangTidyBugproneModule
BranchCloneCheck.cpp
BugproneTidyModule.cpp
CastingThroughVoidCheck.cpp
ChainedComparisonCheck.cpp
ComparePointerToMemberVirtualFunctionCheck.cpp
CopyConstructorInitCheck.cpp
DanglingHandleCheck.cpp

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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>`,

View File

@@ -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;
}

View File

@@ -0,0 +1,91 @@
// RUN: %check_clang_tidy -std=c++98-or-later %s bugprone-chained-comparison %t
void badly_chained_1(int x, int y, int z)
{
bool result = x < y < z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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)
{
bool result = x <= y <= z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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)
{
bool result = x > y > z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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)
{
bool result = x >= y >= z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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)
{
bool result = x == y != z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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(bool x, bool y, bool z)
{
bool result = x != y == z;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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(bool a, bool b, bool c, bool d, bool e, bool f, bool g, bool h)
{
bool result = a == b == c == d == e == f == g == h;
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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(bool v[29])
{
// CHECK-MESSAGES: :[[@LINE+1]]:19: 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]
bool 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)
{
bool result = (x < y) < (z && t) > (a == b);
}
// CHECK-MESSAGES: :[[@LINE-2]]:19: 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)
{
bool result = x && y < z < t && u;
}
// CHECK-MESSAGES: :[[@LINE-2]]:24: 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)
{
bool result = x < y && y < z;
}
void properly_chained_2(int x, int y, bool z)
{
bool result = (x < y) == z;
}
struct Value {
bool operator<(const Value&) const;
};
bool operator==(bool, const Value&);
bool badWithCppOperator(Value a, Value b, Value c) {
return a < b == c;
}
// CHECK-MESSAGES: :[[@LINE-2]]:12: 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]
bool mixedBinaryAndCpp(Value a, Value b, bool c) {
return a < b == c;
}
// CHECK-MESSAGES: :[[@LINE-2]]:12: 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]