-
Notifications
You must be signed in to change notification settings - Fork 15.1k
Add bugprone-loop-variable-copied-then-modified clang-tidy check. #157213
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
base: main
Are you sure you want to change the base?
Changes from all commits
fa8a010
25943ef
57b95c9
88a976e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 "LoopVariableCopiedThenModifiedCheck.h" | ||
| #include "../utils/Matchers.h" | ||
| #include "../utils/TypeTraits.h" | ||
| #include "clang/ASTMatchers/ASTMatchFinder.h" | ||
| #include "clang/Analysis/Analyses/ExprMutationAnalyzer.h" | ||
| #include "clang/Basic/Diagnostic.h" | ||
| #include "clang/Lex/Lexer.h" | ||
|
|
||
| using namespace clang::ast_matchers; | ||
|
|
||
| namespace clang::tidy::bugprone { | ||
|
|
||
| namespace { | ||
| AST_MATCHER(VarDecl, isInMacro) { return Node.getBeginLoc().isMacroID(); } | ||
| } // namespace | ||
|
|
||
| LoopVariableCopiedThenModifiedCheck::LoopVariableCopiedThenModifiedCheck( | ||
| StringRef Name, ClangTidyContext *Context) | ||
| : ClangTidyCheck(Name, Context), IgnoreInexpensiveVariables(Options.get( | ||
| "IgnoreInexpensiveVariables", false)), | ||
| WarnOnlyOnAutoCopies(Options.get("WarnOnlyOnAutoCopies", false)) {} | ||
|
|
||
| void LoopVariableCopiedThenModifiedCheck::storeOptions( | ||
| ClangTidyOptions::OptionMap &Opts) { | ||
| Options.store(Opts, "IgnoreInexpensiveVariables", IgnoreInexpensiveVariables); | ||
| Options.store(Opts, "WarnOnlyOnAutoCopies", WarnOnlyOnAutoCopies); | ||
| } | ||
|
|
||
| void LoopVariableCopiedThenModifiedCheck::registerMatchers( | ||
| MatchFinder *Finder) { | ||
| const auto HasReferenceOrPointerType = hasType(qualType( | ||
| unless(hasCanonicalType(anyOf(referenceType(), pointerType()))))); | ||
| const auto IteratorReturnsValueType = cxxOperatorCallExpr( | ||
| hasOverloadedOperatorName("*"), | ||
| callee( | ||
| cxxMethodDecl(returns(unless(hasCanonicalType(referenceType())))))); | ||
| const auto NotConstructedByCopy = cxxConstructExpr( | ||
| hasDeclaration(cxxConstructorDecl(unless(isCopyConstructor())))); | ||
| const auto ConstructedByConversion = | ||
| cxxMemberCallExpr(callee(cxxConversionDecl())); | ||
| const auto LoopVar = | ||
| varDecl(unless(isInMacro()), HasReferenceOrPointerType, | ||
| unless(hasInitializer(expr(hasDescendant(expr( | ||
| anyOf(materializeTemporaryExpr(), IteratorReturnsValueType, | ||
| NotConstructedByCopy, ConstructedByConversion))))))); | ||
| Finder->addMatcher(cxxForRangeStmt(hasLoopVariable(LoopVar.bind("loopVar"))) | ||
| .bind("forRange"), | ||
| this); | ||
| } | ||
|
|
||
| void LoopVariableCopiedThenModifiedCheck::check( | ||
| const MatchFinder::MatchResult &Result) { | ||
| const auto *LoopVar = Result.Nodes.getNodeAs<VarDecl>("loopVar"); | ||
| std::optional<bool> Expensive = utils::type_traits::isExpensiveToCopy( | ||
| LoopVar->getType(), *Result.Context); | ||
| if ((!Expensive || !*Expensive) && IgnoreInexpensiveVariables) | ||
| return; | ||
|
Comment on lines
+62
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. change logic, if IgnoreInexpensiveVariables is not set, then there is no need to call isExpensiveToCopy, also you could use value_or |
||
| if (WarnOnlyOnAutoCopies) { | ||
| if (!isa<AutoType>(LoopVar->getType())) { | ||
| return; | ||
| } | ||
| } | ||
| const auto *ForRange = Result.Nodes.getNodeAs<CXXForRangeStmt>("forRange"); | ||
|
|
||
| if (!ExprMutationAnalyzer(*ForRange->getBody(), *Result.Context) | ||
| .isMutated(LoopVar)) { | ||
| return; | ||
| } | ||
|
|
||
| clang::SourceRange LoopVarSourceRange = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LoopVarTypeSourceRange |
||
| LoopVar->getTypeSourceInfo()->getTypeLoc().getSourceRange(); | ||
| clang::SourceLocation EndLoc = clang::Lexer::getLocForEndOfToken( | ||
| LoopVarSourceRange.getEnd(), 0, Result.Context->getSourceManager(), | ||
| Result.Context->getLangOpts()); | ||
| diag(LoopVar->getLocation(), | ||
| "loop variable '%0' is copied and then (possibly) modified; use an " | ||
| "explicit copy inside the body of the loop or make the variable a " | ||
| "reference") | ||
| << LoopVar->getName(); | ||
| diag(LoopVar->getLocation(), "consider making '%0' a reference", | ||
| DiagnosticIDs::Note) | ||
| << LoopVar->getName() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for getName, it's safer without it |
||
| << FixItHint::CreateInsertion(LoopVarSourceRange.getBegin(), "const ") | ||
| << FixItHint::CreateInsertion(EndLoc, "&"); | ||
| } | ||
|
|
||
| } // namespace clang::tidy::bugprone | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H | ||
| #define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H | ||
|
|
||
| #include "../ClangTidyCheck.h" | ||
|
|
||
| namespace clang::tidy::bugprone { | ||
|
|
||
| /// Finds loop variables that are copied and subsequently modified. | ||
| /// | ||
| /// For the user-facing documentation see: | ||
| /// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/loop-variable-copied-then-modified.html | ||
| class LoopVariableCopiedThenModifiedCheck : public ClangTidyCheck { | ||
| public: | ||
| LoopVariableCopiedThenModifiedCheck(StringRef Name, | ||
| ClangTidyContext *Context); | ||
| void storeOptions(ClangTidyOptions::OptionMap &Opts) override; | ||
| void registerMatchers(ast_matchers::MatchFinder *Finder) override; | ||
| void check(const ast_matchers::MatchFinder::MatchResult &Result) override; | ||
| bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { | ||
| return LangOpts.CPlusPlus; | ||
| } | ||
|
|
||
| private: | ||
| const bool IgnoreInexpensiveVariables; | ||
flowerhack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const bool WarnOnlyOnAutoCopies; | ||
| }; | ||
|
|
||
| } // namespace clang::tidy::bugprone | ||
|
|
||
| #endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LOOPVARIABLECOPIEDTHENMODIFIEDCHECK_H | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| .. title:: clang-tidy - bugprone-loop-variable-copied-then-modified | ||
|
|
||
| bugprone-loop-variable-copied-then-modified | ||
| =========================================== | ||
|
|
||
| Detects when a loop variable is copied and then subsequently (possibly) modified | ||
flowerhack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| and suggests replacing with a reference or an explicit copy. | ||
|
|
||
| This pattern is considered bugprone because, frequently, programmers do not | ||
| realize that they are modifying a *copy* rather than an underlying value, | ||
| resulting in subtly erroneous code. | ||
|
|
||
| For instance, the following code attempts to null out a value in a map, but only | ||
| succeeds in nulling out a value in a *copy* of the map: | ||
|
|
||
| .. code-block:: c++ | ||
|
|
||
| for (auto target : target_map) { | ||
| target.value = nullptr; | ||
| } | ||
|
|
||
| The programmer is likely to have intended this code instead: | ||
|
|
||
| .. code-block:: c++ | ||
|
|
||
| for (auto& target : target_map) { | ||
| target.value = nullptr; | ||
| } | ||
|
|
||
| This code can be fixed in one of two ways: | ||
| - In cases where the programmer did not intend to create a copy, they can | ||
| convert the loop variable to a reference or a ``const`` reference. A | ||
| fix-note message will provide a naive suggestion of how to achieve this, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do not refer to those fixes as "fix-note", mention somewhere that check provide fixes that can be enabled with --fix-notes. Also mention that those fixes can break compilation.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should common functionality like |
||
| which works in most cases. | ||
| - In cases where the intent is in fact to modify a copy, they may perform the | ||
| copy explicitly, inside the body of the loop, and perform whatever | ||
| operations they like on that copy. | ||
|
|
||
| This is a conservative check: in cases where it cannot be determined at compile | ||
| time whether or not a particular function modifies the variable, it assumes a | ||
| modification has ocurred and warns accordingly. However, in such cases, the | ||
| warning can still be suppressed by doing one of the actions described above. | ||
|
|
||
| Options | ||
| ------- | ||
|
|
||
| .. option:: IgnoreInexpensiveVariables | ||
flowerhack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| When `true`, this check will only alert on types that are expensive to copy. | ||
| This will lead to fewer false positives, but will also overlook some | ||
| instances where there may be an actual bug. Default is `false`. | ||
flowerhack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| .. option:: WarnOnlyOnAutoCopies | ||
|
|
||
| When `true`, this check will only alert on `auto` types. Default is `false`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // RUN: %check_clang_tidy -std=c++17-or-later %s bugprone-loop-variable-copied-then-modified %t --fix-notes | ||
|
|
||
| template <typename T> | ||
| struct Iterator { | ||
| void operator++() {} | ||
| const T& operator*() { | ||
| static T* TT = new T(); | ||
| return *TT; | ||
| } | ||
| bool operator!=(const Iterator &) { return false; } | ||
| }; | ||
| template <typename T> | ||
| struct View { | ||
| T begin() { return T(); } | ||
| T begin() const { return T(); } | ||
| T end() { return T(); } | ||
| T end() const { return T(); } | ||
| }; | ||
|
|
||
| struct ConstructorConvertible { | ||
| }; | ||
|
|
||
| struct S { | ||
| int value; | ||
|
|
||
| S() : value(0) {}; | ||
| S(const S &); | ||
| S(const ConstructorConvertible&) {} | ||
| ~S(); | ||
| S &operator=(const S &); | ||
| void modify() { | ||
| value++; | ||
| } | ||
| }; | ||
|
|
||
| struct Convertible { | ||
| operator S() const { | ||
| return S(); | ||
| } | ||
| }; | ||
|
|
||
| struct PairLike { | ||
| int id; | ||
| S data; | ||
| }; | ||
|
|
||
| template <typename V> | ||
| struct Generic { | ||
| V value; | ||
|
|
||
| Generic() : value{} {}; | ||
| Generic(const Generic &); | ||
| ~Generic(); | ||
| Generic &operator=(const Generic &); | ||
| void modify() { | ||
| value++; | ||
| } | ||
| }; | ||
|
|
||
| void PositiveLoopVariableCopiedAndThenModfiedGeneric() { | ||
| for (Generic G : View<Iterator<Generic<double>>>()) { | ||
| // 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 | ||
| // CHECK-MESSAGES: [[@LINE-2]]:16: note: consider making 'G' a reference | ||
| // CHECK-FIXES: for (const Generic& G : View<Iterator<Generic<double>>>()) { | ||
| G.modify(); | ||
| } | ||
| } | ||
|
|
||
| void NegativeLoopVariableIsReferenceAndModifiedGeneric() { | ||
| for (const Generic<double>& G : View<Iterator<Generic<double>>>()) { | ||
| // It's fine to copy-by-value G into some other G. | ||
| Generic<double> G2 = G; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // RUN: %check_clang_tidy %s bugprone-loop-variable-copied-then-modified %t --fix-notes \ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing tests with "auto&&" and "const auto" |
||
| // RUN: -config="{CheckOptions: \ | ||
| // RUN: {bugprone-loop-variable-copied-then-modified.IgnoreInexpensiveVariables: true}}" \ | ||
| // RUN: -- -I%S | ||
| #include "Inputs/system-header-simulator/sim_initializer_list" | ||
| #include "Inputs/system-header-simulator/sim_vector" | ||
|
|
||
| template <typename T> | ||
| struct Iterator { | ||
| void operator++() {} | ||
| const T& operator*() { | ||
| static T* TT = new T(); | ||
| return *TT; | ||
| } | ||
| bool operator!=(const Iterator &) { return false; } | ||
| }; | ||
| template <typename T> | ||
| struct View { | ||
| T begin() { return T(); } | ||
| T begin() const { return T(); } | ||
| T end() { return T(); } | ||
| T end() const { return T(); } | ||
| }; | ||
|
|
||
| struct S { | ||
| int value; | ||
|
|
||
| S() : value(0) {}; | ||
| S(const S &); | ||
| ~S(); | ||
| S &operator=(const S &); | ||
| void modify() { | ||
| value++; | ||
| } | ||
| }; | ||
|
|
||
| void NegativeOnlyCopyingInts() { | ||
| std::vector<int> foo; | ||
| foo.push_back(1); | ||
| foo.push_back(2); | ||
| foo.push_back(3); | ||
| for (int v : foo) { | ||
| v += 1; | ||
| } | ||
| } | ||
|
|
||
| void PositiveLoopVariableCopiedAndThenModfied() { | ||
| for (S S1 : View<Iterator<S>>()) { | ||
| // 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 | ||
| // CHECK-MESSAGES: [[@LINE-2]]:10: note: consider making 'S1' a reference | ||
| // CHECK-FIXES: for (const S& S1 : View<Iterator<S>>()) { | ||
| S1.modify(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider excluding system headers or implicit code like template instances, otherwise there could be conflicts on fixes