Skip to content

Commit 9261aeb

Browse files
committed
Add WarnOnModificationOfCopiedLoopVariable to
performance-for-range-copy. Adds an option to performance-for-range-copy that alerts when a loop variable is copied and modified. This is a bugprone pattern because the programmer in this case often assumes they are modifying the original value instead of a copy. The warning can be suppressed by instead explicitly copying the value inside the body of the loop.
1 parent ddf9b91 commit 9261aeb

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

clang-tools-extra/clang-tidy/performance/ForRangeCopyCheck.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ namespace clang::tidy::performance {
2222
ForRangeCopyCheck::ForRangeCopyCheck(StringRef Name, ClangTidyContext *Context)
2323
: ClangTidyCheck(Name, Context),
2424
WarnOnAllAutoCopies(Options.get("WarnOnAllAutoCopies", false)),
25+
WarnOnModificationOfCopiedLoopVariable(
26+
Options.get("WarnOnModificationOfCopiedLoopVariable", false)),
2527
AllowedTypes(
2628
utils::options::parseStringList(Options.get("AllowedTypes", ""))) {}
2729

2830
void ForRangeCopyCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
2931
Options.store(Opts, "WarnOnAllAutoCopies", WarnOnAllAutoCopies);
32+
Options.store(Opts, "WarnOnModificationOfCopiedLoopVariable",
33+
WarnOnModificationOfCopiedLoopVariable);
3034
Options.store(Opts, "AllowedTypes",
3135
utils::options::serializeStringList(AllowedTypes));
3236
}
@@ -64,9 +68,11 @@ void ForRangeCopyCheck::check(const MatchFinder::MatchResult &Result) {
6468
// Ignore code in macros since we can't place the fixes correctly.
6569
if (Var->getBeginLoc().isMacroID())
6670
return;
71+
const auto *ForRange = Result.Nodes.getNodeAs<CXXForRangeStmt>("forRange");
72+
if (copiedLoopVarIsMutated(*Var, *ForRange, *Result.Context))
73+
return;
6774
if (handleConstValueCopy(*Var, *Result.Context))
6875
return;
69-
const auto *ForRange = Result.Nodes.getNodeAs<CXXForRangeStmt>("forRange");
7076
handleCopyIsOnlyConstReferenced(*Var, *ForRange, *Result.Context);
7177
}
7278

@@ -79,10 +85,15 @@ bool ForRangeCopyCheck::handleConstValueCopy(const VarDecl &LoopVar,
7985
} else if (!LoopVar.getType().isConstQualified()) {
8086
return false;
8187
}
88+
89+
// TODO: Check if this is actually needed for some cases? It seems redundant
90+
// with the Expensive check in handleCopyIsOnlyConstReferenced but maybe
91+
// I'm missing something
8292
std::optional<bool> Expensive =
8393
utils::type_traits::isExpensiveToCopy(LoopVar.getType(), Context);
8494
if (!Expensive || !*Expensive)
8595
return false;
96+
8697
auto Diagnostic =
8798
diag(LoopVar.getLocation(),
8899
"the loop variable's type is not a reference type; this creates a "
@@ -105,13 +116,35 @@ static bool isReferenced(const VarDecl &LoopVar, const Stmt &Stmt,
105116
.empty();
106117
}
107118

119+
bool ForRangeCopyCheck::copiedLoopVarIsMutated(const VarDecl &LoopVar,
120+
const CXXForRangeStmt &ForRange,
121+
ASTContext &Context) {
122+
// If it's copied and mutated, there's a high chance that's a bug.
123+
if (WarnOnModificationOfCopiedLoopVariable) {
124+
if (ExprMutationAnalyzer(*ForRange.getBody(), Context)
125+
.isMutated(&LoopVar)) {
126+
auto Diag =
127+
diag(LoopVar.getLocation(), "loop variable is copied and then "
128+
"modified, which is likely a bug; you "
129+
"probably want to modify the underlying "
130+
"object and not this copy. If you "
131+
"*did* intend to modify this copy, "
132+
"please use an explicit copy inside the "
133+
"body of the loop");
134+
return true;
135+
}
136+
}
137+
return false;
138+
}
139+
108140
bool ForRangeCopyCheck::handleCopyIsOnlyConstReferenced(
109141
const VarDecl &LoopVar, const CXXForRangeStmt &ForRange,
110142
ASTContext &Context) {
111143
std::optional<bool> Expensive =
112144
utils::type_traits::isExpensiveToCopy(LoopVar.getType(), Context);
113145
if (LoopVar.getType().isConstQualified() || !Expensive || !*Expensive)
114146
return false;
147+
115148
// We omit the case where the loop variable is not used in the loop body. E.g.
116149
//
117150
// for (auto _ : benchmark_state) {
@@ -130,7 +163,6 @@ bool ForRangeCopyCheck::handleCopyIsOnlyConstReferenced(
130163
if (std::optional<FixItHint> Fix = utils::fixit::addQualifierToVarDecl(
131164
LoopVar, Context, Qualifiers::Const))
132165
Diag << *Fix << utils::fixit::changeVarDeclToReference(LoopVar, Context);
133-
134166
return true;
135167
}
136168
return false;

clang-tools-extra/clang-tidy/performance/ForRangeCopyCheck.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ class ForRangeCopyCheck : public ClangTidyCheck {
3939
const CXXForRangeStmt &ForRange,
4040
ASTContext &Context);
4141

42+
// Checks if the loop variable is copied and then subsequently mutated
43+
// in the body of the loop. If so it suggests the copy was unintentional,
44+
// or, that the copy would be more clear if done inside the body of the loop.
45+
bool copiedLoopVarIsMutated(const VarDecl &LoopVar,
46+
const CXXForRangeStmt &ForRange,
47+
ASTContext &Context);
48+
4249
const bool WarnOnAllAutoCopies;
4350
const std::vector<StringRef> AllowedTypes;
51+
const bool WarnOnModificationOfCopiedLoopVariable;
4452
};
4553

4654
} // namespace clang::tidy::performance

clang-tools-extra/docs/clang-tidy/checks/performance/for-range-copy.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ following heuristic is employed:
1818
invoked on it, or it is used as const reference or value argument in
1919
constructors or function calls.
2020

21+
There is an additional option to warn any time a loop variable is copied in each
22+
iteration and subsequently modified in the body of the loop. This is likely to
23+
be a bug, since the user may believe they are modifying the underlying value
24+
instead of a copy, and a user that truly intends to modify a copy may do so by
25+
performing the copy explicitly inside the body of the loop.
26+
2127
Options
2228
-------
2329

@@ -26,6 +32,12 @@ Options
2632
When `true`, warns on any use of ``auto`` as the type of the range-based for
2733
loop variable. Default is `false`.
2834

35+
.. option:: WarnOnModificationOfCopiedLoopVariable
36+
37+
When `true`, warns when the loop variable is copied and subsequently
38+
modified. This is likely to be a bug. Moving the copy to an explicit copy
39+
inside of the loop will suppress the warning. Default is `false`.
40+
2941
.. option:: AllowedTypes
3042

3143
A semicolon-separated list of names of types allowed to be copied in each
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// RUN: %check_clang_tidy %s performance-for-range-copy %t -- \
2+
// RUN: -config="{CheckOptions: {performance-for-range-copy.WarnOnModificationOfCopiedLoopVariable: true}}"
3+
4+
template <typename T>
5+
struct Iterator {
6+
void operator++() {}
7+
const T& operator*() {
8+
static T* TT = new T();
9+
return *TT;
10+
}
11+
bool operator!=(const Iterator &) { return false; }
12+
};
13+
template <typename T>
14+
struct View {
15+
T begin() { return T(); }
16+
T begin() const { return T(); }
17+
T end() { return T(); }
18+
T end() const { return T(); }
19+
};
20+
21+
struct S {
22+
int value;
23+
24+
S() : value(0) {};
25+
S(const S &);
26+
~S();
27+
S &operator=(const S &);
28+
void modify() {
29+
value++;
30+
}
31+
};
32+
33+
void NegativeLoopVariableNotCopied() {
34+
for (const S& S1 : View<Iterator<S>>()) {
35+
}
36+
}
37+
38+
void NegativeLoopVariableCopiedButNotModified() {
39+
for (S S1 : View<Iterator<S>>()) {
40+
}
41+
}
42+
43+
void PositiveLoopVariableCopiedAndThenModfied() {
44+
for (S S1 : View<Iterator<S>>()) {
45+
// CHECK-MESSAGES: [[@LINE-1]]:10: warning: loop variable is copied and then modified, which is likely a bug; you probably want to modify the underlying object and not this copy. If you *did* intend to modify this copy, please use an explicit copy inside the body of the loop
46+
S1.modify();
47+
}
48+
}
49+

0 commit comments

Comments
 (0)