Skip to content

Commit fa8a010

Browse files
committed
Add bugprone-loop-variable-copied-then-modified clang-tidy check.
Adds a clang-tidy check that alerts when a loop variable is copied and subsequently modified. This is a bugprone pattern because the programmer in this case often assumes they are modifying the original value instead of a copy. This warning can be suppressed by either converting the loop variable to a const ref, or by performing the copy explicitly inside the body of the loop. Fix llvmGH-155922
1 parent 1efa997 commit fa8a010

11 files changed

+534
-0
lines changed

clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include "IntegerDivisionCheck.h"
4141
#include "InvalidEnumDefaultInitializationCheck.h"
4242
#include "LambdaFunctionNameCheck.h"
43+
#include "LoopVariableCopiedThenModifiedCheck.h"
4344
#include "MacroParenthesesCheck.h"
4445
#include "MacroRepeatedSideEffectsCheck.h"
4546
#include "MisleadingSetterOfReferenceCheck.h"
@@ -154,6 +155,8 @@ class BugproneModule : public ClangTidyModule {
154155
"bugprone-incorrect-enable-if");
155156
CheckFactories.registerCheck<IncorrectEnableSharedFromThisCheck>(
156157
"bugprone-incorrect-enable-shared-from-this");
158+
CheckFactories.registerCheck<LoopVariableCopiedThenModifiedCheck>(
159+
"bugprone-loop-variable-copied-then-modified");
157160
CheckFactories.registerCheck<UnintendedCharOstreamOutputCheck>(
158161
"bugprone-unintended-char-ostream-output");
159162
CheckFactories.registerCheck<ReturnConstRefFromParameterCheck>(

clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ add_clang_library(clangTidyBugproneModule STATIC
3131
IncorrectEnableIfCheck.cpp
3232
IncorrectEnableSharedFromThisCheck.cpp
3333
InvalidEnumDefaultInitializationCheck.cpp
34+
LoopVariableCopiedThenModifiedCheck.cpp
3435
UnintendedCharOstreamOutputCheck.cpp
3536
ReturnConstRefFromParameterCheck.cpp
3637
SuspiciousStringviewDataUsageCheck.cpp
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "LoopVariableCopiedThenModifiedCheck.h"
10+
#include "../utils/Matchers.h"
11+
#include "../utils/TypeTraits.h"
12+
#include "clang/ASTMatchers/ASTMatchFinder.h"
13+
#include "clang/Analysis/Analyses/ExprMutationAnalyzer.h"
14+
#include "clang/Basic/Diagnostic.h"
15+
#include "clang/Lex/Lexer.h"
16+
17+
using namespace clang::ast_matchers;
18+
19+
namespace clang::tidy::bugprone {
20+
21+
namespace {
22+
AST_MATCHER(VarDecl, isInMacro) { return Node.getBeginLoc().isMacroID(); }
23+
} // namespace
24+
25+
LoopVariableCopiedThenModifiedCheck::LoopVariableCopiedThenModifiedCheck(
26+
StringRef Name, ClangTidyContext *Context)
27+
: ClangTidyCheck(Name, Context), IgnoreInexpensiveVariables(Options.get(
28+
"IgnoreInexpensiveVariables", false)),
29+
WarnOnlyOnAutoCopies(Options.get("WarnOnlyOnAutoCopies", false)) {}
30+
31+
void LoopVariableCopiedThenModifiedCheck::storeOptions(
32+
ClangTidyOptions::OptionMap &Opts) {
33+
Options.store(Opts, "IgnoreInexpensiveVariables", IgnoreInexpensiveVariables);
34+
Options.store(Opts, "WarnOnlyOnAutoCopies", WarnOnlyOnAutoCopies);
35+
}
36+
37+
void LoopVariableCopiedThenModifiedCheck::registerMatchers(
38+
MatchFinder *Finder) {
39+
const auto HasReferenceOrPointerType = hasType(qualType(
40+
unless(hasCanonicalType(anyOf(referenceType(), pointerType())))));
41+
const auto IteratorReturnsValueType = cxxOperatorCallExpr(
42+
hasOverloadedOperatorName("*"),
43+
callee(
44+
cxxMethodDecl(returns(unless(hasCanonicalType(referenceType()))))));
45+
const auto NotConstructedByCopy = cxxConstructExpr(
46+
hasDeclaration(cxxConstructorDecl(unless(isCopyConstructor()))));
47+
const auto ConstructedByConversion =
48+
cxxMemberCallExpr(callee(cxxConversionDecl()));
49+
const auto LoopVar =
50+
varDecl(unless(isInMacro()), HasReferenceOrPointerType,
51+
unless(hasInitializer(expr(hasDescendant(expr(
52+
anyOf(materializeTemporaryExpr(), IteratorReturnsValueType,
53+
NotConstructedByCopy, ConstructedByConversion)))))));
54+
Finder->addMatcher(cxxForRangeStmt(hasLoopVariable(LoopVar.bind("loopVar")))
55+
.bind("forRange"),
56+
this);
57+
}
58+
59+
void LoopVariableCopiedThenModifiedCheck::check(
60+
const MatchFinder::MatchResult &Result) {
61+
const auto *LoopVar = Result.Nodes.getNodeAs<VarDecl>("loopVar");
62+
std::optional<bool> Expensive = utils::type_traits::isExpensiveToCopy(
63+
LoopVar->getType(), *Result.Context);
64+
if ((!Expensive || !*Expensive) && IgnoreInexpensiveVariables)
65+
return;
66+
if (WarnOnlyOnAutoCopies) {
67+
if (!isa<AutoType>(LoopVar->getType())) {
68+
return;
69+
}
70+
}
71+
const auto *ForRange = Result.Nodes.getNodeAs<CXXForRangeStmt>("forRange");
72+
73+
if (!ExprMutationAnalyzer(*ForRange->getBody(), *Result.Context)
74+
.isMutated(LoopVar)) {
75+
return;
76+
}
77+
78+
clang::SourceRange LoopVarSourceRange =
79+
LoopVar->getTypeSourceInfo()->getTypeLoc().getSourceRange();
80+
clang::SourceLocation EndLoc = clang::Lexer::getLocForEndOfToken(
81+
LoopVarSourceRange.getEnd(), 0, Result.Context->getSourceManager(),
82+
Result.Context->getLangOpts());
83+
diag(LoopVar->getLocation(),
84+
"loop variable '%0' is copied and then (possibly) modified; use an "
85+
"explicit copy inside the body of the loop or make the variable a "
86+
"reference")
87+
<< LoopVar->getName();
88+
diag(LoopVar->getLocation(), "consider making '%0' a reference",
89+
DiagnosticIDs::Note)
90+
<< LoopVar->getName()
91+
<< FixItHint::CreateInsertion(LoopVarSourceRange.getBegin(), "const ")
92+
<< FixItHint::CreateInsertion(EndLoc, "&");
93+
}
94+
95+
} // namespace clang::tidy::bugprone
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::bugprone {
15+
16+
/// Finds loop variables that are copied and subsequently modified.
17+
///
18+
/// For the user-facing documentation see:
19+
/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/loop-variable-copied-then-modified.html
20+
class LoopVariableCopiedThenModifiedCheck : public ClangTidyCheck {
21+
public:
22+
LoopVariableCopiedThenModifiedCheck(StringRef Name,
23+
ClangTidyContext *Context);
24+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
25+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
26+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
27+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28+
return LangOpts.CPlusPlus;
29+
}
30+
31+
private:
32+
const bool IgnoreInexpensiveVariables;
33+
const bool WarnOnlyOnAutoCopies;
34+
};
35+
36+
} // namespace clang::tidy::bugprone
37+
38+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,23 @@ Improvements to clang-tidy
142142
New checks
143143
^^^^^^^^^^
144144

145+
- New :doc:`bugprone-derived-method-shadowing-base-method
146+
<clang-tidy/checks/bugprone/derived-method-shadowing-base-method>` check.
147+
148+
Finds derived class methods that shadow a (non-virtual) base class method.
149+
145150
- New :doc:`bugprone-invalid-enum-default-initialization
146151
<clang-tidy/checks/bugprone/invalid-enum-default-initialization>` check.
147152

148153
Detects default initialization (to 0) of variables with ``enum`` type where
149154
the enum has no enumerator with value of 0.
150155

156+
- New :doc:`bugprone-loop-variable-copied-then-modified
157+
<clang-tidy/checks/bugprone/loop-variable-copied-then-modified>` check.
158+
159+
Detects when a loop variable is copied and then subsequently (possibly) modified
160+
and suggests replacing with a reference or an explicit copy.
161+
151162
- New :doc:`cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
152163
<clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access>`
153164
check.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.. title:: clang-tidy - bugprone-loop-variable-copied-then-modified
2+
3+
bugprone-loop-variable-copied-then-modified
4+
===========================================
5+
6+
Detects when a loop variable is copied and then subsequently (possibly) modified
7+
and suggests replacing with a reference or an explicit copy.
8+
9+
This pattern is considered bugprone because, frequently, programmers do not
10+
realize that they are modifying a *copy* rather than an underlying value,
11+
resulting in subtly erroneous code.
12+
13+
For instance, the following code attempts to null out a value in a map, but only
14+
succeeds in nulling out a value in a *copy* of the map:
15+
16+
.. code-block:: c++
17+
18+
for (auto target : target_map) {
19+
target.value = nullptr;
20+
}
21+
22+
The programmer is likely to have intended this code instead:
23+
24+
.. code-block:: c++
25+
26+
for (auto& target : target_map) {
27+
target.value = nullptr;
28+
}
29+
30+
This code can be fixed in one of two ways:
31+
- In cases where the programmer did not intend to create a copy, they can
32+
convert the loop variable to a reference or a ``const`` reference. A
33+
fix-note message will provide a naive suggestion of how to achieve this,
34+
which works in most cases.
35+
- In cases where the intent is in fact to modify a copy, they may perform the
36+
copy explicitly, inside the body of the loop, and perform whatever
37+
operations they like on that copy.
38+
39+
This is a conservative check: in cases where it cannot be determined at compile
40+
time whether or not a particular function modifies the variable, it assumes a
41+
modification has ocurred and warns accordingly. However, in such cases, the
42+
warning can still be suppressed by doing one of the actions described above.
43+
44+
Options
45+
-------
46+
47+
.. option:: IgnoreInexpensiveVariables
48+
49+
When `true`, this check will only alert on types that are expensive to copy.
50+
This will lead to fewer false positives, but will also overlook some
51+
instances where there may be an actual bug. Default is `false`.
52+
53+
.. option:: WarnOnlyOnAutoCopies
54+
55+
When `true`, this check will only alert on `auto` types. Default is `false`.

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Clang-Tidy Checks
108108
:doc:`bugprone-integer-division <bugprone/integer-division>`,
109109
:doc:`bugprone-invalid-enum-default-initialization <bugprone/invalid-enum-default-initialization>`,
110110
:doc:`bugprone-lambda-function-name <bugprone/lambda-function-name>`,
111+
:doc:`bugprone-loop-variable-copied-then-modified <bugprone/loop-variable-copied-then-modified>`, "Yes"
111112
:doc:`bugprone-macro-parentheses <bugprone/macro-parentheses>`, "Yes"
112113
:doc:`bugprone-macro-repeated-side-effects <bugprone/macro-repeated-side-effects>`,
113114
:doc:`bugprone-misleading-setter-of-reference <bugprone/misleading-setter-of-reference>`,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// RUN: %check_clang_tidy -std=c++17-or-later %s bugprone-loop-variable-copied-then-modified %t --fix-notes
2+
3+
template <typename T>
4+
struct Iterator {
5+
void operator++() {}
6+
const T& operator*() {
7+
static T* TT = new T();
8+
return *TT;
9+
}
10+
bool operator!=(const Iterator &) { return false; }
11+
};
12+
template <typename T>
13+
struct View {
14+
T begin() { return T(); }
15+
T begin() const { return T(); }
16+
T end() { return T(); }
17+
T end() const { return T(); }
18+
};
19+
20+
struct ConstructorConvertible {
21+
};
22+
23+
struct S {
24+
int value;
25+
26+
S() : value(0) {};
27+
S(const S &);
28+
S(const ConstructorConvertible&) {}
29+
~S();
30+
S &operator=(const S &);
31+
void modify() {
32+
value++;
33+
}
34+
};
35+
36+
struct Convertible {
37+
operator S() const {
38+
return S();
39+
}
40+
};
41+
42+
struct PairLike {
43+
int id;
44+
S data;
45+
};
46+
47+
template <typename V>
48+
struct Generic {
49+
V value;
50+
51+
Generic() : value{} {};
52+
Generic(const Generic &);
53+
~Generic();
54+
Generic &operator=(const Generic &);
55+
void modify() {
56+
value++;
57+
}
58+
};
59+
60+
void PositiveLoopVariableCopiedAndThenModfiedGeneric() {
61+
for (Generic G : View<Iterator<Generic<double>>>()) {
62+
// CHECK-MESSAGES: [[@LINE-1]]:16: warning: loop variable 'G' is copied and then (possibly) modified; use an explicit copy inside the body of the loop or make the variable a reference
63+
// CHECK-MESSAGES: [[@LINE-2]]:16: note: consider making 'G' a reference
64+
// CHECK-FIXES: for (const Generic& G : View<Iterator<Generic<double>>>()) {
65+
G.modify();
66+
}
67+
}
68+
69+
void NegativeLoopVariableIsReferenceAndModifiedGeneric() {
70+
for (const Generic<double>& G : View<Iterator<Generic<double>>>()) {
71+
// It's fine to copy-by-value G into some other G.
72+
Generic<double> G2 = G;
73+
}
74+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// RUN: %check_clang_tidy %s bugprone-loop-variable-copied-then-modified %t --fix-notes \
2+
// RUN: -config="{CheckOptions: \
3+
// RUN: {bugprone-loop-variable-copied-then-modified.IgnoreInexpensiveVariables: true}}" \
4+
// RUN: -- -I%S
5+
#include "Inputs/system-header-simulator/sim_initializer_list"
6+
#include "Inputs/system-header-simulator/sim_vector"
7+
8+
template <typename T>
9+
struct Iterator {
10+
void operator++() {}
11+
const T& operator*() {
12+
static T* TT = new T();
13+
return *TT;
14+
}
15+
bool operator!=(const Iterator &) { return false; }
16+
};
17+
template <typename T>
18+
struct View {
19+
T begin() { return T(); }
20+
T begin() const { return T(); }
21+
T end() { return T(); }
22+
T end() const { return T(); }
23+
};
24+
25+
struct S {
26+
int value;
27+
28+
S() : value(0) {};
29+
S(const S &);
30+
~S();
31+
S &operator=(const S &);
32+
void modify() {
33+
value++;
34+
}
35+
};
36+
37+
void NegativeOnlyCopyingInts() {
38+
std::vector<int> foo;
39+
foo.push_back(1);
40+
foo.push_back(2);
41+
foo.push_back(3);
42+
for (int v : foo) {
43+
v += 1;
44+
}
45+
}
46+
47+
void PositiveLoopVariableCopiedAndThenModfied() {
48+
for (S S1 : View<Iterator<S>>()) {
49+
// CHECK-MESSAGES: [[@LINE-1]]:10: warning: loop variable 'S1' is copied and then (possibly) modified; use an explicit copy inside the body of the loop or make the variable a reference
50+
// CHECK-MESSAGES: [[@LINE-2]]:10: note: consider making 'S1' a reference
51+
// CHECK-FIXES: for (const S& S1 : View<Iterator<S>>()) {
52+
S1.modify();
53+
}
54+
}

0 commit comments

Comments
 (0)