Skip to content

Commit 95171f2

Browse files
nicovankdl8sd11
andcommitted
[clang-tidy] Support find for string-like classes in readability-container-contains
Fix llvm#109327. Small parts taken from llvm#110351. Removed the type checking between `contains` and `count`/`find` arguments for simplicity. Because of overloads, this type-checking is tricky. The same strategy is used in `modernize-use-starts-ends-with`. Ended up copying over the FixIt logic from `modernize-use-starts-ends-with`, I think it's the simplest. Open to other suggestions. Since this may introduce false positives in (bad readability) edge cases, I tested this change on several major C++ projects, and didn't find any new build or test failures introduced by this change (some errors due to C++23 upgrade, unrelated). Tested projects: 1. kitware/cmake 2. dolphin-emu/dolphin 3. blender/blender 4. opencv/opencv Co-authored-by: dl8sd11 <[email protected]>
1 parent 07d0225 commit 95171f2

File tree

5 files changed

+115
-61
lines changed

5 files changed

+115
-61
lines changed

clang-tools-extra/clang-tidy/readability/ContainerContainsCheck.cpp

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,43 @@
99
#include "ContainerContainsCheck.h"
1010
#include "clang/AST/ASTContext.h"
1111
#include "clang/ASTMatchers/ASTMatchFinder.h"
12+
#include "clang/Lex/Lexer.h"
1213

1314
using namespace clang::ast_matchers;
1415

1516
namespace clang::tidy::readability {
1617
void ContainerContainsCheck::registerMatchers(MatchFinder *Finder) {
17-
const auto HasContainsMatchingParamType = hasMethod(
18-
cxxMethodDecl(isConst(), parameterCountIs(1), returns(booleanType()),
19-
hasName("contains"), unless(isDeleted()), isPublic(),
20-
hasParameter(0, hasType(hasUnqualifiedDesugaredType(
21-
equalsBoundNode("parameterType"))))));
18+
const auto Literal0 = integerLiteral(equals(0));
19+
const auto Literal1 = integerLiteral(equals(1));
20+
21+
const auto ClassWithContains = cxxRecordDecl(
22+
hasMethod(cxxMethodDecl(isConst(), parameterCountIs(1), isPublic(),
23+
unless(isDeleted()), returns(booleanType()),
24+
hasAnyName("contains", "Contains"))
25+
.bind("contains_fun")));
2226

2327
const auto CountCall =
24-
cxxMemberCallExpr(
25-
argumentCountIs(1),
26-
callee(cxxMethodDecl(
27-
hasName("count"),
28-
hasParameter(0, hasType(hasUnqualifiedDesugaredType(
29-
type().bind("parameterType")))),
30-
ofClass(cxxRecordDecl(HasContainsMatchingParamType)))))
28+
cxxMemberCallExpr(argumentCountIs(1),
29+
callee(cxxMethodDecl(hasAnyName("count", "Count"),
30+
ofClass(ClassWithContains))))
3131
.bind("call");
3232

3333
const auto FindCall =
34-
cxxMemberCallExpr(
35-
argumentCountIs(1),
36-
callee(cxxMethodDecl(
37-
hasName("find"),
38-
hasParameter(0, hasType(hasUnqualifiedDesugaredType(
39-
type().bind("parameterType")))),
40-
ofClass(cxxRecordDecl(HasContainsMatchingParamType)))))
34+
// Either one argument, or assume the second argument is the position to
35+
// start searching from.
36+
cxxMemberCallExpr(anyOf(argumentCountIs(1),
37+
cxxMemberCallExpr(argumentCountIs(2),
38+
hasArgument(1, Literal0))),
39+
callee(cxxMethodDecl(hasAnyName("find", "Find"),
40+
ofClass(ClassWithContains))))
4141
.bind("call");
4242

4343
const auto EndCall = cxxMemberCallExpr(
44-
argumentCountIs(0),
45-
callee(
46-
cxxMethodDecl(hasName("end"),
47-
// In the matchers below, FindCall should always appear
48-
// before EndCall so 'parameterType' is properly bound.
49-
ofClass(cxxRecordDecl(HasContainsMatchingParamType)))));
44+
argumentCountIs(0), callee(cxxMethodDecl(hasAnyName("end", "End"),
45+
ofClass(ClassWithContains))));
5046

51-
const auto Literal0 = integerLiteral(equals(0));
52-
const auto Literal1 = integerLiteral(equals(1));
47+
const auto StringNpos = anyOf(declRefExpr(to(varDecl(hasName("npos")))),
48+
memberExpr(member(hasName("npos"))));
5349

5450
auto AddSimpleMatcher = [&](auto Matcher) {
5551
Finder->addMatcher(
@@ -94,12 +90,14 @@ void ContainerContainsCheck::registerMatchers(MatchFinder *Finder) {
9490
binaryOperation(hasLHS(Literal1), hasOperatorName(">"), hasRHS(CountCall))
9591
.bind("negativeComparison"));
9692

97-
// Find membership tests based on `find() == end()`.
93+
// Find membership tests based on `find() == end()` or `find() == npos`.
9894
AddSimpleMatcher(
99-
binaryOperation(hasOperatorName("!="), hasOperands(FindCall, EndCall))
95+
binaryOperation(hasOperatorName("!="),
96+
hasOperands(FindCall, anyOf(EndCall, StringNpos)))
10097
.bind("positiveComparison"));
10198
AddSimpleMatcher(
102-
binaryOperation(hasOperatorName("=="), hasOperands(FindCall, EndCall))
99+
binaryOperation(hasOperatorName("=="),
100+
hasOperands(FindCall, anyOf(EndCall, StringNpos)))
103101
.bind("negativeComparison"));
104102
}
105103

@@ -114,29 +112,39 @@ void ContainerContainsCheck::check(const MatchFinder::MatchResult &Result) {
114112
"only one of PositiveComparison or NegativeComparison should be set");
115113
bool Negated = NegativeComparison != nullptr;
116114
const auto *Comparison = Negated ? NegativeComparison : PositiveComparison;
115+
const StringRef ContainsFunName =
116+
Result.Nodes.getNodeAs<CXXMethodDecl>("contains_fun")->getName();
117+
const Expr *SearchExpr = Call->getArg(0)->IgnoreParenImpCasts();
117118

118119
// Diagnose the issue.
119-
auto Diag =
120-
diag(Call->getExprLoc(), "use 'contains' to check for membership");
120+
auto Diag = diag(Call->getExprLoc(), "use '%0' to check for membership")
121+
<< ContainsFunName;
121122

122123
// Don't fix it if it's in a macro invocation. Leave fixing it to the user.
123124
SourceLocation FuncCallLoc = Comparison->getEndLoc();
124125
if (!FuncCallLoc.isValid() || FuncCallLoc.isMacroID())
125126
return;
126127

127-
// Create the fix it.
128-
const auto *Member = cast<MemberExpr>(Call->getCallee());
129-
Diag << FixItHint::CreateReplacement(
130-
Member->getMemberNameInfo().getSourceRange(), "contains");
131-
SourceLocation ComparisonBegin = Comparison->getSourceRange().getBegin();
132-
SourceLocation ComparisonEnd = Comparison->getSourceRange().getEnd();
133-
SourceLocation CallBegin = Call->getSourceRange().getBegin();
134-
SourceLocation CallEnd = Call->getSourceRange().getEnd();
128+
const StringRef SearchExprText = Lexer::getSourceText(
129+
CharSourceRange::getTokenRange(SearchExpr->getSourceRange()),
130+
*Result.SourceManager, Result.Context->getLangOpts());
131+
132+
// Remove everything before the function call.
133+
Diag << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
134+
Comparison->getBeginLoc(), Call->getBeginLoc()));
135+
136+
// Rename the function to `contains`.
137+
Diag << FixItHint::CreateReplacement(Call->getExprLoc(), ContainsFunName);
138+
139+
// Replace arguments and everything after the function call.
135140
Diag << FixItHint::CreateReplacement(
136-
CharSourceRange::getCharRange(ComparisonBegin, CallBegin),
137-
Negated ? "!" : "");
138-
Diag << FixItHint::CreateRemoval(CharSourceRange::getTokenRange(
139-
CallEnd.getLocWithOffset(1), ComparisonEnd));
141+
CharSourceRange::getTokenRange(Call->getArg(0)->getBeginLoc(),
142+
Comparison->getEndLoc()),
143+
(SearchExprText + ")").str());
144+
145+
// Add negation if necessary.
146+
if (Negated)
147+
Diag << FixItHint::CreateInsertion(Call->getBeginLoc(), "!");
140148
}
141149

142150
} // namespace clang::tidy::readability

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ Changes in existing checks
230230
<clang-tidy/checks/portability/template-virtual-member-function>` check to
231231
avoid false positives on pure virtual member functions.
232232

233+
- Improved :doc:`readability-container-contains
234+
<clang-tidy/checks/readability/container-contains>` to support string
235+
comparisons to ``npos``.
236+
233237
- Improved :doc:`readability-container-size-empty
234238
<clang-tidy/checks/readability/container-size-empty>` check by correctly
235239
generating fix-it hints when size method is called from implicit ``this``,

clang-tools-extra/docs/clang-tidy/checks/readability/container-contains.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Initial expression Result
2020
-------------------------------------- -------------------------------------
2121
``myMap.find(x) == myMap.end()`` ``!myMap.contains(x)``
2222
``myMap.find(x) != myMap.end()`` ``myMap.contains(x)``
23+
``myStr.find(x) != std::string::npos`` ``myStr.contains(x)``
2324
``if (myMap.count(x))`` ``if (myMap.contains(x))``
2425
``bool exists = myMap.count(x)`` ``bool exists = myMap.contains(x)``
2526
``bool exists = myMap.count(x) > 0`` ``bool exists = myMap.contains(x)``

clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/string

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,19 @@ struct basic_string {
5050
size_type find(const _Type& str, size_type pos = 0) const;
5151
size_type find(const C* s, size_type pos = 0) const;
5252
size_type find(const C* s, size_type pos, size_type n) const;
53+
size_type find(C ch, size_type pos = 0) const;
54+
template<class StringViewLike>
55+
size_type find(const StringViewLike& t, size_type pos = 0) const;
5356

5457
size_type rfind(const _Type& str, size_type pos = npos) const;
5558
size_type rfind(const C* s, size_type pos, size_type count) const;
5659
size_type rfind(const C* s, size_type pos = npos) const;
5760
size_type rfind(C ch, size_type pos = npos) const;
5861

62+
constexpr bool contains(std::basic_string_view<C, T> sv) const noexcept;
63+
constexpr bool contains(C ch) const noexcept;
64+
constexpr bool contains(const C* s) const;
65+
5966
_Type& insert(size_type pos, const _Type& str);
6067
_Type& insert(size_type pos, const C* s);
6168
_Type& insert(size_type pos, const C* s, size_type n);
@@ -110,6 +117,10 @@ struct basic_string_view {
110117
size_type rfind(const C* s, size_type pos, size_type count) const;
111118
size_type rfind(const C* s, size_type pos = npos) const;
112119

120+
constexpr bool contains(basic_string_view sv) const noexcept;
121+
constexpr bool contains(C ch) const noexcept;
122+
constexpr bool contains(const C* s) const;
123+
113124
constexpr bool starts_with(basic_string_view sv) const noexcept;
114125
constexpr bool starts_with(C ch) const noexcept;
115126
constexpr bool starts_with(const C* s) const;

clang-tools-extra/test/clang-tidy/checkers/readability/container-contains.cpp

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
// RUN: %check_clang_tidy -std=c++11-or-later %s readability-container-contains %t
1+
// RUN: %check_clang_tidy -std=c++11-or-later %s readability-container-contains %t -- \
2+
// RUN: -- -isystem %clang_tidy_headers
3+
4+
#include <string>
25

36
// Some *very* simplified versions of `map` etc.
47
namespace std {
@@ -318,17 +321,15 @@ void testPrivateContains(CustomMapPrivateContains<int, int> &MyMap,
318321
if (MyMap2.count(0)) {};
319322
}
320323

321-
struct MyString {};
322-
323324
struct WeirdNonMatchingContains {
324325
unsigned count(char) const;
325-
bool contains(const MyString&) const;
326+
bool contains(const std::string&) const;
326327
};
327328

328-
void testWeirdNonMatchingContains(WeirdNonMatchingContains &MyMap) {
329-
// No warning if there is no `contains` method with the right type.
330-
if (MyMap.count('a')) {};
331-
}
329+
// False positives: when count/find and contains take different types,
330+
// the check will suggest an invalid code transformation. These cases
331+
// should not exist in real code or be rare enough.
332+
// void f(WeirdNonMatchingContains MyMap) { MyMap.count('a'); }
332333

333334
template <class T>
334335
struct SmallPtrSet {
@@ -375,15 +376,6 @@ void testSubclassEntry(SmallPtrSet<X>& Set, Y* Entry) {
375376
// CHECK-FIXES: if (Set.contains(Entry)) {}
376377
}
377378

378-
struct WeirdPointerApi {
379-
unsigned count(int** Ptr) const;
380-
bool contains(int* Ptr) const;
381-
};
382-
383-
void testWeirdApi(WeirdPointerApi& Set, int* E) {
384-
if (Set.count(&E)) {}
385-
}
386-
387379
void testIntUnsigned(std::set<int>& S, unsigned U) {
388380
if (S.count(U)) {}
389381
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
@@ -458,3 +450,41 @@ void testOperandPermutations(std::map<int, int>& Map) {
458450
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
459451
// CHECK-FIXES: if (!Map.contains(0)) {};
460452
}
453+
454+
void testStringNpos(std::string Str1, std::string Str2, std::string_view StrView) {
455+
if (Str1.find(Str2) == std::string::npos) {};
456+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
457+
// CHECK-FIXES: if (!Str1.contains(Str2)) {};
458+
459+
if (Str1.find("test") == std::string::npos) {};
460+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
461+
// CHECK-FIXES: if (!Str1.contains("test")) {};
462+
463+
if (Str1.find('c') != std::string::npos) {};
464+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
465+
// CHECK-FIXES: if (Str1.contains('c')) {};
466+
467+
if (Str1.find(Str2, 0) != std::string::npos) {};
468+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
469+
// CHECK-FIXES: if (Str1.contains(Str2)) {};
470+
471+
if (std::string::npos == Str1.find("test", 0)) {};
472+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
473+
// CHECK-FIXES: if (!Str1.contains("test")) {};
474+
475+
if (StrView.find("test") == std::string_view::npos) {};
476+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
477+
// CHECK-FIXES: if (!StrView.contains("test")) {};
478+
479+
if (StrView.find('c') != std::string_view::npos) {};
480+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
481+
// CHECK-FIXES: if (StrView.contains('c')) {};
482+
483+
if (StrView.find('c') != StrView.npos) {};
484+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use 'contains' to check for membership [readability-container-contains]
485+
// CHECK-FIXES: if (StrView.contains('c')) {};
486+
487+
// These should not match.
488+
if (Str1.find('c', 1) != Str1.npos) {};
489+
if (Str1.find(StrView, 1) != Str1.npos) {};
490+
}

0 commit comments

Comments
 (0)