Skip to content

Commit ef59069

Browse files
authored
[clang-tidy][modernize-use-starts-ends-with] Add support for compare() (#89530)
Using `compare` is the next most common roundabout way to express `starts_with` before it was added to the standard. In this case, using `starts_with` is a readability improvement. Extend existing `modernize-use-starts-ends-with` to cover this case. ``` // The following will now be replaced by starts_with(). string.compare(0, strlen("prefix"), "prefix") == 0; string.compare(0, 6, "prefix") == 0; string.compare(0, prefix.length(), prefix) == 0; string.compare(0, prefix.size(), prefix) == 0; ```
1 parent 5ac744d commit ef59069

File tree

8 files changed

+162
-22
lines changed

8 files changed

+162
-22
lines changed

clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
4343
callee(cxxMethodDecl(hasName("find")).bind("find_fun")),
4444
// ... on a class with a starts_with function.
4545
on(hasType(
46-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
46+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
47+
// Bind search expression.
48+
hasArgument(0, expr().bind("search_expr")));
4749

4850
const auto RFindExpr = cxxMemberCallExpr(
4951
// A method call with a second argument of zero...
@@ -52,15 +54,68 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
5254
callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
5355
// ... on a class with a starts_with function.
5456
on(hasType(
55-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
57+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
58+
// Bind search expression.
59+
hasArgument(0, expr().bind("search_expr")));
60+
61+
// Match a string literal and an integer or strlen() call matching the length.
62+
const auto HasStringLiteralAndLengthArgs = [](const auto StringArgIndex,
63+
const auto LengthArgIndex) {
64+
return allOf(
65+
hasArgument(StringArgIndex, stringLiteral().bind("string_literal_arg")),
66+
hasArgument(LengthArgIndex,
67+
anyOf(integerLiteral().bind("integer_literal_size_arg"),
68+
callExpr(callee(functionDecl(parameterCountIs(1),
69+
hasName("strlen"))),
70+
hasArgument(0, stringLiteral().bind(
71+
"strlen_arg"))))));
72+
};
73+
74+
// Match a string variable and a call to length() or size().
75+
const auto HasStringVariableAndSizeCallArgs = [](const auto StringArgIndex,
76+
const auto LengthArgIndex) {
77+
return allOf(
78+
hasArgument(StringArgIndex, declRefExpr(hasDeclaration(
79+
decl().bind("string_var_decl")))),
80+
hasArgument(LengthArgIndex,
81+
cxxMemberCallExpr(
82+
callee(cxxMethodDecl(isConst(), parameterCountIs(0),
83+
hasAnyName("size", "length"))),
84+
on(declRefExpr(
85+
to(decl(equalsBoundNode("string_var_decl"))))))));
86+
};
5687

57-
const auto FindOrRFindExpr =
58-
cxxMemberCallExpr(anyOf(FindExpr, RFindExpr)).bind("find_expr");
88+
// Match either one of the two cases above.
89+
const auto HasStringAndLengthArgs =
90+
[HasStringLiteralAndLengthArgs, HasStringVariableAndSizeCallArgs](
91+
const auto StringArgIndex, const auto LengthArgIndex) {
92+
return anyOf(
93+
HasStringLiteralAndLengthArgs(StringArgIndex, LengthArgIndex),
94+
HasStringVariableAndSizeCallArgs(StringArgIndex, LengthArgIndex));
95+
};
96+
97+
const auto CompareExpr = cxxMemberCallExpr(
98+
// A method call with three arguments...
99+
argumentCountIs(3),
100+
// ... where the first argument is zero...
101+
hasArgument(0, ZeroLiteral),
102+
// ... named compare...
103+
callee(cxxMethodDecl(hasName("compare")).bind("find_fun")),
104+
// ... on a class with a starts_with function...
105+
on(hasType(
106+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
107+
// ... where the third argument is some string and the second a length.
108+
HasStringAndLengthArgs(2, 1),
109+
// Bind search expression.
110+
hasArgument(2, expr().bind("search_expr")));
59111

60112
Finder->addMatcher(
61-
// Match [=!]= with a zero on one side and a string.(r?)find on the other.
62-
binaryOperator(hasAnyOperatorName("==", "!="),
63-
hasOperands(FindOrRFindExpr, ZeroLiteral))
113+
// Match [=!]= with a zero on one side and (r?)find|compare on the other.
114+
binaryOperator(
115+
hasAnyOperatorName("==", "!="),
116+
hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr))
117+
.bind("find_expr"),
118+
ZeroLiteral))
64119
.bind("expr"),
65120
this);
66121
}
@@ -69,23 +124,42 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
69124
const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
70125
const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
71126
const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
127+
const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("search_expr");
72128
const auto *StartsWithFunction =
73129
Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
74130

131+
const auto *StringLiteralArg =
132+
Result.Nodes.getNodeAs<StringLiteral>("string_literal_arg");
133+
const auto *IntegerLiteralSizeArg =
134+
Result.Nodes.getNodeAs<IntegerLiteral>("integer_literal_size_arg");
135+
const auto *StrlenArg = Result.Nodes.getNodeAs<StringLiteral>("strlen_arg");
136+
137+
// Filter out compare cases where the length does not match string literal.
138+
if (StringLiteralArg && IntegerLiteralSizeArg &&
139+
StringLiteralArg->getLength() !=
140+
IntegerLiteralSizeArg->getValue().getZExtValue()) {
141+
return;
142+
}
143+
144+
if (StringLiteralArg && StrlenArg &&
145+
StringLiteralArg->getLength() != StrlenArg->getLength()) {
146+
return;
147+
}
148+
75149
if (ComparisonExpr->getBeginLoc().isMacroID()) {
76150
return;
77151
}
78152

79153
const bool Neg = ComparisonExpr->getOpcode() == BO_NE;
80154

81155
auto Diagnostic =
82-
diag(FindExpr->getBeginLoc(), "use %0 instead of %1() %select{==|!=}2 0")
156+
diag(FindExpr->getExprLoc(), "use %0 instead of %1() %select{==|!=}2 0")
83157
<< StartsWithFunction->getName() << FindFun->getName() << Neg;
84158

85-
// Remove possible zero second argument and ' [!=]= 0' suffix.
159+
// Remove possible arguments after search expression and ' [!=]= 0' suffix.
86160
Diagnostic << FixItHint::CreateReplacement(
87161
CharSourceRange::getTokenRange(
88-
Lexer::getLocForEndOfToken(FindExpr->getArg(0)->getEndLoc(), 0,
162+
Lexer::getLocForEndOfToken(SearchExpr->getEndLoc(), 0,
89163
*Result.SourceManager, getLangOpts()),
90164
ComparisonExpr->getEndLoc()),
91165
")");
@@ -94,11 +168,12 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
94168
Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
95169
ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
96170

97-
// Replace '(r?)find' with 'starts_with'.
171+
// Replace method name by 'starts_with'.
172+
// Remove possible arguments before search expression.
98173
Diagnostic << FixItHint::CreateReplacement(
99-
CharSourceRange::getTokenRange(FindExpr->getExprLoc(),
100-
FindExpr->getExprLoc()),
101-
StartsWithFunction->getName());
174+
CharSourceRange::getCharRange(FindExpr->getExprLoc(),
175+
SearchExpr->getBeginLoc()),
176+
(StartsWithFunction->getName() + "(").str());
102177

103178
// Add possible negation '!'.
104179
if (Neg) {

clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
namespace clang::tidy::modernize {
1515

16-
/// Checks whether a ``find`` or ``rfind`` result is compared with 0 and
17-
/// suggests replacing with ``starts_with`` when the method exists in the class.
18-
/// Notably, this will work with ``std::string`` and ``std::string_view``.
16+
/// Checks for common roundabout ways to express ``starts_with`` and
17+
/// ``ends_with`` and suggests replacing with ``starts_with`` when the method is
18+
/// available. Notably, this will work with ``std::string`` and
19+
/// ``std::string_view``.
1920
///
2021
/// For the user-facing documentation see:
2122
/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/use-starts-ends-with.html

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ Changes in existing checks
266266
<clang-tidy/checks/modernize/use-override>` check to also remove any trailing
267267
whitespace when deleting the ``virtual`` keyword.
268268

269+
- Improved :doc:`modernize-use-starts-ends-with
270+
<clang-tidy/checks/modernize/use-starts-ends-with>` check to also handle
271+
calls to ``compare`` method.
272+
269273
- Improved :doc:`modernize-use-using <clang-tidy/checks/modernize/use-using>`
270274
check by adding support for detection of typedefs declared on function level.
271275

clang-tools-extra/docs/clang-tidy/checks/modernize/use-starts-ends-with.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
modernize-use-starts-ends-with
44
==============================
55

6-
Checks whether a ``find`` or ``rfind`` result is compared with 0 and suggests
7-
replacing with ``starts_with`` when the method exists in the class. Notably,
8-
this will work with ``std::string`` and ``std::string_view``.
6+
Checks for common roundabout ways to express ``starts_with`` and ``ends_with``
7+
and suggests replacing with ``starts_with`` when the method is available.
8+
Notably, this will work with ``std::string`` and ``std::string_view``.
99

1010
.. code-block:: c++
1111

1212
std::string s = "...";
1313
if (s.find("prefix") == 0) { /* do something */ }
1414
if (s.rfind("prefix", 0) == 0) { /* do something */ }
15+
if (s.compare(0, strlen("prefix"), "prefix") == 0) { /* do something */ }
1516
1617
becomes
1718

@@ -20,3 +21,4 @@ becomes
2021
std::string s = "...";
2122
if (s.starts_with("prefix")) { /* do something */ }
2223
if (s.starts_with("prefix")) { /* do something */ }
24+
if (s.starts_with("prefix")) { /* do something */ }

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ struct basic_string {
4444
int compare(const C* s) const;
4545
int compare(size_type pos, size_type len, const _Type&) const;
4646
int compare(size_type pos, size_type len, const C* s) const;
47+
template<class StringViewLike>
48+
int compare(size_type pos1, size_type count1, const StringViewLike& t) const;
4749

4850
size_type find(const _Type& str, size_type pos = 0) const;
4951
size_type find(const C* s, size_type pos = 0) const;
@@ -129,6 +131,8 @@ bool operator!=(const char*, const std::string&);
129131
bool operator==(const std::wstring&, const std::wstring&);
130132
bool operator==(const std::wstring&, const wchar_t*);
131133
bool operator==(const wchar_t*, const std::wstring&);
134+
135+
size_t strlen(const char* str);
132136
}
133137

134138
#endif // _STRING_

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
#include "stddef.h"
1313

1414
void *memcpy(void *dest, const void *src, size_t n);
15+
size_t strlen(const char* str);
1516

1617
#endif // _STRING_H_

clang-tools-extra/test/clang-tidy/checkers/abseil/redundant-strcat-calls.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// RUN: %check_clang_tidy %s abseil-redundant-strcat-calls %t -- -- -isystem %clang_tidy_headers
22
#include <string>
33

4-
int strlen(const char *);
5-
64
namespace absl {
75

86
class string_view {

clang-tools-extra/test/clang-tidy/checkers/modernize/use-starts-ends-with.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// RUN: %check_clang_tidy -std=c++20 %s modernize-use-starts-ends-with %t -- \
22
// RUN: -- -isystem %clang_tidy_headers
33

4+
#include <string.h>
45
#include <string>
56

67
std::string foo(std::string);
@@ -158,10 +159,64 @@ void test(std::string s, std::string_view sv, sub_string ss, sub_sub_string sss,
158159
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use startsWith
159160
// CHECK-FIXES: puvi.startsWith("a");
160161

162+
s.compare(0, 1, "a") == 0;
163+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with instead of compare() == 0
164+
// CHECK-FIXES: s.starts_with("a");
165+
166+
s.compare(0, 1, "a") != 0;
167+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with instead of compare() != 0
168+
// CHECK-FIXES: !s.starts_with("a");
169+
170+
s.compare(0, strlen("a"), "a") == 0;
171+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
172+
// CHECK-FIXES: s.starts_with("a");
173+
174+
s.compare(0, std::strlen("a"), "a") == 0;
175+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
176+
// CHECK-FIXES: s.starts_with("a");
177+
178+
s.compare(0, std::strlen(("a")), "a") == 0;
179+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
180+
// CHECK-FIXES: s.starts_with("a");
181+
182+
s.compare(0, std::strlen(("a")), (("a"))) == 0;
183+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
184+
// CHECK-FIXES: s.starts_with("a");
185+
186+
s.compare(0, s.size(), s) == 0;
187+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
188+
// CHECK-FIXES: s.starts_with(s);
189+
190+
s.compare(0, s.length(), s) == 0;
191+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
192+
// CHECK-FIXES: s.starts_with(s);
193+
194+
0 != s.compare(0, sv.length(), sv);
195+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
196+
// CHECK-FIXES: s.starts_with(sv);
197+
198+
#define LENGTH(x) (x).length()
199+
s.compare(0, LENGTH(s), s) == 0;
200+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
201+
// CHECK-FIXES: s.starts_with(s);
202+
203+
s.compare(ZERO, LENGTH(s), s) == ZERO;
204+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
205+
// CHECK-FIXES: s.starts_with(s);
206+
207+
s.compare(ZERO, LENGTH(sv), sv) != 0;
208+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
209+
// CHECK-FIXES: !s.starts_with(sv);
210+
161211
// Expressions that don't trigger the check are here.
162212
#define EQ(x, y) ((x) == (y))
163213
EQ(s.find("a"), 0);
164214

165215
#define DOTFIND(x, y) (x).find(y)
166216
DOTFIND(s, "a") == 0;
217+
218+
#define STARTS_WITH_COMPARE(x, y) (x).compare(0, (x).size(), (y))
219+
STARTS_WITH_COMPARE(s, s) == 0;
220+
221+
s.compare(0, 1, "ab") == 0;
167222
}

0 commit comments

Comments
 (0)