Skip to content

Commit 6fae5b4

Browse files
committed
[clang-tidy][readability-container-contains] Extend to any class with contains
This check will now work out of the box with other containers that have a `contains` method, such as `folly::F14` or Abseil containers. It will also work with strings, which are basically just weird containers. `std::string` and `std::string_view` will have a `contains` method starting with C++23. `llvm::StringRef` and `folly::StringPiece` are examples of existing implementations with a `contains` method.
1 parent 86e5c54 commit 6fae5b4

File tree

5 files changed

+322
-90
lines changed

5 files changed

+322
-90
lines changed

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

Lines changed: 126 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,89 +14,156 @@ using namespace clang::ast_matchers;
1414

1515
namespace clang::tidy::readability {
1616

17+
namespace {
18+
struct NotMatchingBoundType {
19+
NotMatchingBoundType(std::string ArgumentTypeBoundID, DynTypedNode Node)
20+
: ArgumentTypeBoundID(std::move(ArgumentTypeBoundID)),
21+
Node(std::move(Node)) {}
22+
bool operator()(const ast_matchers::internal::BoundNodesMap &Nodes) const {
23+
const Type *ParamType = Node.get<Type>();
24+
const Type *ArgType = Nodes.getNodeAs<Type>(ArgumentTypeBoundID);
25+
if (!ParamType || !ArgType) {
26+
return true;
27+
}
28+
29+
ParamType = ParamType->getUnqualifiedDesugaredType();
30+
ArgType = ArgType->getUnqualifiedDesugaredType();
31+
32+
if (ParamType->isReferenceType()) {
33+
ParamType = ParamType->getPointeeType()->getUnqualifiedDesugaredType();
34+
}
35+
36+
while (ParamType->isPointerType()) {
37+
if (!ArgType->isPointerType()) {
38+
return true;
39+
}
40+
41+
ParamType = ParamType->getPointeeType()->getUnqualifiedDesugaredType();
42+
ArgType = ArgType->getPointeeType()->getUnqualifiedDesugaredType();
43+
}
44+
45+
return ParamType != ArgType;
46+
}
47+
48+
private:
49+
std::string ArgumentTypeBoundID;
50+
DynTypedNode Node;
51+
};
52+
53+
AST_MATCHER_P(Type, matchesBoundType, std::string, ArgumentTypeBoundID) {
54+
return Builder->removeBindings(
55+
NotMatchingBoundType(ArgumentTypeBoundID, DynTypedNode::create(Node)));
56+
}
57+
} // namespace
58+
1759
void ContainerContainsCheck::registerMatchers(MatchFinder *Finder) {
18-
const auto SupportedContainers = hasType(
19-
hasUnqualifiedDesugaredType(recordType(hasDeclaration(cxxRecordDecl(
20-
hasAnyName("::std::set", "::std::unordered_set", "::std::map",
21-
"::std::unordered_map", "::std::multiset",
22-
"::std::unordered_multiset", "::std::multimap",
23-
"::std::unordered_multimap"))))));
60+
const auto HasContainsMethod = hasMethod(cxxMethodDecl(
61+
isConst(), parameterCountIs(1), returns(booleanType()),
62+
hasName("contains"), unless(isDeleted()), isPublic(),
63+
hasParameter(0, hasType(matchesBoundType("argumentType")))));
64+
const auto ContainerWithContains = hasType(
65+
hasUnqualifiedDesugaredType(recordType(hasDeclaration(cxxRecordDecl(anyOf(
66+
HasContainsMethod, hasAnyBase(hasType(hasCanonicalType(hasDeclaration(
67+
cxxRecordDecl(HasContainsMethod)))))))))));
68+
69+
// Hack in CountCall and FindCall: hasArgument(0, ...) always ignores implicit
70+
// casts. We do not want this behavior. We use hasAnyArgument instead, which
71+
// does not ignore implicit casts. Thankfully, we only have one argument in
72+
// every case making this possible. Really, hasArgument should also respect
73+
// the current traversal mode. GitHub issues #54919 and #75754 track this.
2474

2575
const auto CountCall =
26-
cxxMemberCallExpr(on(SupportedContainers),
76+
cxxMemberCallExpr(argumentCountIs(1),
2777
callee(cxxMethodDecl(hasName("count"))),
28-
argumentCountIs(1))
78+
hasAnyArgument(hasType(type().bind("argumentType"))),
79+
on(ContainerWithContains))
2980
.bind("call");
3081

3182
const auto FindCall =
32-
cxxMemberCallExpr(on(SupportedContainers),
83+
cxxMemberCallExpr(argumentCountIs(1),
3384
callee(cxxMethodDecl(hasName("find"))),
34-
argumentCountIs(1))
85+
hasAnyArgument(hasType(type().bind("argumentType"))),
86+
on(ContainerWithContains))
3587
.bind("call");
3688

37-
const auto EndCall = cxxMemberCallExpr(on(SupportedContainers),
89+
const auto EndCall = cxxMemberCallExpr(argumentCountIs(0),
3890
callee(cxxMethodDecl(hasName("end"))),
39-
argumentCountIs(0));
91+
on(ContainerWithContains));
4092

4193
const auto Literal0 = integerLiteral(equals(0));
4294
const auto Literal1 = integerLiteral(equals(1));
4395

44-
auto AddSimpleMatcher = [&](auto Matcher) {
45-
Finder->addMatcher(
46-
traverse(TK_IgnoreUnlessSpelledInSource, std::move(Matcher)), this);
47-
};
48-
4996
// Find membership tests which use `count()`.
5097
Finder->addMatcher(implicitCastExpr(hasImplicitDestinationType(booleanType()),
5198
hasSourceExpression(CountCall))
5299
.bind("positiveComparison"),
53100
this);
54-
AddSimpleMatcher(
55-
binaryOperator(hasLHS(CountCall), hasOperatorName("!="), hasRHS(Literal0))
56-
.bind("positiveComparison"));
57-
AddSimpleMatcher(
58-
binaryOperator(hasLHS(Literal0), hasOperatorName("!="), hasRHS(CountCall))
59-
.bind("positiveComparison"));
60-
AddSimpleMatcher(
61-
binaryOperator(hasLHS(CountCall), hasOperatorName(">"), hasRHS(Literal0))
62-
.bind("positiveComparison"));
63-
AddSimpleMatcher(
64-
binaryOperator(hasLHS(Literal0), hasOperatorName("<"), hasRHS(CountCall))
65-
.bind("positiveComparison"));
66-
AddSimpleMatcher(
67-
binaryOperator(hasLHS(CountCall), hasOperatorName(">="), hasRHS(Literal1))
68-
.bind("positiveComparison"));
69-
AddSimpleMatcher(
70-
binaryOperator(hasLHS(Literal1), hasOperatorName("<="), hasRHS(CountCall))
71-
.bind("positiveComparison"));
101+
Finder->addMatcher(
102+
binaryOperator(hasOperatorName("!="),
103+
hasOperands(ignoringParenImpCasts(CountCall),
104+
ignoringParenImpCasts(Literal0)))
105+
.bind("positiveComparison"),
106+
this);
107+
Finder->addMatcher(binaryOperator(hasOperatorName(">"),
108+
hasLHS(ignoringParenImpCasts(CountCall)),
109+
hasRHS(ignoringParenImpCasts(Literal0)))
110+
.bind("positiveComparison"),
111+
this);
112+
Finder->addMatcher(binaryOperator(hasOperatorName("<"),
113+
hasLHS(ignoringParenImpCasts(Literal0)),
114+
hasRHS(ignoringParenImpCasts(CountCall)))
115+
.bind("positiveComparison"),
116+
this);
117+
Finder->addMatcher(binaryOperator(hasOperatorName(">="),
118+
hasLHS(ignoringParenImpCasts(CountCall)),
119+
hasRHS(ignoringParenImpCasts(Literal1)))
120+
.bind("positiveComparison"),
121+
this);
122+
Finder->addMatcher(binaryOperator(hasOperatorName("<="),
123+
hasLHS(ignoringParenImpCasts(Literal1)),
124+
hasRHS(ignoringParenImpCasts(CountCall)))
125+
.bind("positiveComparison"),
126+
this);
72127

73128
// Find inverted membership tests which use `count()`.
74-
AddSimpleMatcher(
75-
binaryOperator(hasLHS(CountCall), hasOperatorName("=="), hasRHS(Literal0))
76-
.bind("negativeComparison"));
77-
AddSimpleMatcher(
78-
binaryOperator(hasLHS(Literal0), hasOperatorName("=="), hasRHS(CountCall))
79-
.bind("negativeComparison"));
80-
AddSimpleMatcher(
81-
binaryOperator(hasLHS(CountCall), hasOperatorName("<="), hasRHS(Literal0))
82-
.bind("negativeComparison"));
83-
AddSimpleMatcher(
84-
binaryOperator(hasLHS(Literal0), hasOperatorName(">="), hasRHS(CountCall))
85-
.bind("negativeComparison"));
86-
AddSimpleMatcher(
87-
binaryOperator(hasLHS(CountCall), hasOperatorName("<"), hasRHS(Literal1))
88-
.bind("negativeComparison"));
89-
AddSimpleMatcher(
90-
binaryOperator(hasLHS(Literal1), hasOperatorName(">"), hasRHS(CountCall))
91-
.bind("negativeComparison"));
129+
Finder->addMatcher(
130+
binaryOperator(hasOperatorName("=="),
131+
hasOperands(ignoringParenImpCasts(CountCall),
132+
ignoringParenImpCasts(Literal0)))
133+
.bind("negativeComparison"),
134+
this);
135+
Finder->addMatcher(binaryOperator(hasOperatorName("<="),
136+
hasLHS(ignoringParenImpCasts(CountCall)),
137+
hasRHS(ignoringParenImpCasts(Literal0)))
138+
.bind("negativeComparison"),
139+
this);
140+
Finder->addMatcher(binaryOperator(hasOperatorName(">="),
141+
hasLHS(ignoringParenImpCasts(Literal0)),
142+
hasRHS(ignoringParenImpCasts(CountCall)))
143+
.bind("negativeComparison"),
144+
this);
145+
Finder->addMatcher(binaryOperator(hasOperatorName("<"),
146+
hasLHS(ignoringParenImpCasts(CountCall)),
147+
hasRHS(ignoringParenImpCasts(Literal1)))
148+
.bind("negativeComparison"),
149+
this);
150+
Finder->addMatcher(binaryOperator(hasOperatorName(">"),
151+
hasLHS(ignoringParenImpCasts(Literal1)),
152+
hasRHS(ignoringParenImpCasts(CountCall)))
153+
.bind("negativeComparison"),
154+
this);
92155

93156
// Find membership tests based on `find() == end()`.
94-
AddSimpleMatcher(
95-
binaryOperator(hasLHS(FindCall), hasOperatorName("!="), hasRHS(EndCall))
96-
.bind("positiveComparison"));
97-
AddSimpleMatcher(
98-
binaryOperator(hasLHS(FindCall), hasOperatorName("=="), hasRHS(EndCall))
99-
.bind("negativeComparison"));
157+
Finder->addMatcher(binaryOperator(hasOperatorName("!="),
158+
hasOperands(ignoringParenImpCasts(FindCall),
159+
ignoringParenImpCasts(EndCall)))
160+
.bind("positiveComparison"),
161+
this);
162+
Finder->addMatcher(binaryOperator(hasOperatorName("=="),
163+
hasOperands(ignoringParenImpCasts(FindCall),
164+
ignoringParenImpCasts(EndCall)))
165+
.bind("negativeComparison"),
166+
this);
100167
}
101168

102169
void ContainerContainsCheck::check(const MatchFinder::MatchResult &Result) {

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
namespace clang::tidy::readability {
1515

16-
/// Finds usages of `container.count()` and `find() == end()` which should be
17-
/// replaced by a call to the `container.contains()` method introduced in C++20.
16+
/// Finds usages of ``container.count()`` and
17+
/// ``container.find() == container.end()`` which should be replaced by a call
18+
/// to the ``container.contains()`` method.
1819
///
1920
/// For the user-facing documentation see:
2021
/// http://clang.llvm.org/extra/clang-tidy/checks/readability/container-contains.html
@@ -24,10 +25,11 @@ class ContainerContainsCheck : public ClangTidyCheck {
2425
: ClangTidyCheck(Name, Context) {}
2526
void registerMatchers(ast_matchers::MatchFinder *Finder) final;
2627
void check(const ast_matchers::MatchFinder::MatchResult &Result) final;
27-
28-
protected:
2928
bool isLanguageVersionSupported(const LangOptions &LO) const final {
30-
return LO.CPlusPlus20;
29+
return LO.CPlusPlus;
30+
}
31+
std::optional<TraversalKind> getCheckTraversalKind() const override {
32+
return TK_AsIs;
3133
}
3234
};
3335

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ Changes in existing checks
123123
<clang-tidy/checks/modernize/use-std-print>` check to support replacing
124124
member function calls too.
125125

126-
- Improved :doc:`readablility-implicit-bool-conversion
126+
- Improved :doc:`readability-container-contains
127+
<clang-tidy/checks/readability/container-contains>` check
128+
to let it work on any class that has a ``contains`` method.
129+
130+
- Improved :doc:`readability-implicit-bool-conversion
127131
<clang-tidy/checks/readability/implicit-bool-conversion>` check
128132
by adding the option `UseUpperCaseLiteralSuffix` to select the
129133
case of the literal suffix in fixes.

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@
33
readability-container-contains
44
==============================
55

6-
Finds usages of ``container.count()`` and ``container.find() == container.end()`` which should be replaced by a call to the ``container.contains()`` method introduced in C++20.
6+
Finds usages of ``container.count()`` and
7+
``container.find() == container.end()`` which should be replaced by a call to
8+
the ``container.contains()`` method.
79

8-
Whether an element is contained inside a container should be checked with ``contains`` instead of ``count``/``find`` because ``contains`` conveys the intent more clearly. Furthermore, for containers which permit multiple entries per key (``multimap``, ``multiset``, ...), ``contains`` is more efficient than ``count`` because ``count`` has to do unnecessary additional work.
10+
Whether an element is contained inside a container should be checked with
11+
``contains`` instead of ``count``/``find`` because ``contains`` conveys the
12+
intent more clearly. Furthermore, for containers which permit multiple entries
13+
per key (``multimap``, ``multiset``, ...), ``contains`` is more efficient than
14+
``count`` because ``count`` has to do unnecessary additional work.
915

1016
Examples:
1117

12-
=========================================== ==============================
13-
Initial expression Result
14-
------------------------------------------- ------------------------------
15-
``myMap.find(x) == myMap.end()`` ``!myMap.contains(x)``
16-
``myMap.find(x) != myMap.end()`` ``myMap.contains(x)``
17-
``if (myMap.count(x))`` ``if (myMap.contains(x))``
18-
``bool exists = myMap.count(x)`` ``bool exists = myMap.contains(x)``
19-
``bool exists = myMap.count(x) > 0`` ``bool exists = myMap.contains(x)``
20-
``bool exists = myMap.count(x) >= 1`` ``bool exists = myMap.contains(x)``
21-
``bool missing = myMap.count(x) == 0`` ``bool missing = !myMap.contains(x)``
22-
=========================================== ==============================
18+
========================================= ==============================
19+
Initial expression Result
20+
----------------------------------------- ------------------------------
21+
``myMap.find(x) == myMap.end()`` ``!myMap.contains(x)``
22+
``myMap.find(x) != myMap.end()`` ``myMap.contains(x)``
23+
``if (myMap.count(x))`` ``if (myMap.contains(x))``
24+
``bool exists = myMap.count(x)`` ``bool exists = myMap.contains(x)``
25+
``bool exists = myMap.count(x) > 0`` ``bool exists = myMap.contains(x)``
26+
``bool exists = myMap.count(x) >= 1`` ``bool exists = myMap.contains(x)``
27+
``bool missing = myMap.count(x) == 0`` ``bool missing = !myMap.contains(x)``
28+
========================================= ==============================
2329

24-
This check applies to ``std::set``, ``std::unordered_set``, ``std::map``, ``std::unordered_map`` and the corresponding multi-key variants.
25-
It is only active for C++20 and later, as the ``contains`` method was only added in C++20.
30+
This check will apply to any class that has a ``contains`` method, notably
31+
including ``std::set``, ``std::unordered_set``, ``std::map``, and
32+
``std::unordered_map`` as of C++20, and ``std::string`` and ``std::string_view``
33+
as of C++23.

0 commit comments

Comments
 (0)