Skip to content

Commit 5c21b1b

Browse files
nicovankdl8sd11
andauthored
[clang-tidy] Support find for string-like classes in readability-container-contains (#157243)
Fix #109327. Small parts taken from #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`. Co-authored-by: dl8sd11 <[email protected]>
1 parent efd96af commit 5c21b1b

File tree

5 files changed

+158
-99
lines changed

5 files changed

+158
-99
lines changed

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

Lines changed: 51 additions & 43 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+
// Either one argument, or assume the second argument is the position to
35+
// start searching from.
3436
cxxMemberCallExpr(
35-
argumentCountIs(1),
36-
callee(cxxMethodDecl(
37-
hasName("find"),
38-
hasParameter(0, hasType(hasUnqualifiedDesugaredType(
39-
type().bind("parameterType")))),
40-
ofClass(cxxRecordDecl(HasContainsMatchingParamType)))))
37+
anyOf(argumentCountIs(1),
38+
allOf(argumentCountIs(2), 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ Changes in existing checks
312312
<clang-tidy/checks/portability/template-virtual-member-function>` check to
313313
avoid false positives on pure virtual member functions.
314314

315+
- Improved :doc:`readability-container-contains
316+
<clang-tidy/checks/readability/container-contains>` to support string
317+
comparisons to ``npos``. Internal changes may cause new rare false positives
318+
in non-standard containers.
319+
315320
- Improved :doc:`readability-container-size-empty
316321
<clang-tidy/checks/readability/container-size-empty>` check by correctly
317322
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;

0 commit comments

Comments
 (0)