Skip to content

Commit ff7ebb3

Browse files
committed
[clang-tidy][modernize-use-starts-ends-with] Add support for compare()
1 parent 56ca5ec commit ff7ebb3

File tree

8 files changed

+163
-21
lines changed

8 files changed

+163
-21
lines changed

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

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,75 @@
1616
using namespace clang::ast_matchers;
1717

1818
namespace clang::tidy::modernize {
19+
namespace {
20+
// Given two argument indices X and Y, matches when a call expression has a
21+
// string at index X with an expression representing that string's length at
22+
// index Y. The string can be a string literal or a variable. The length can be
23+
// matched via an integer literal or a call to strlen() in the case of a string
24+
// literal, and by a call to size() or length() in the string variable case.
25+
AST_POLYMORPHIC_MATCHER_P2(hasStringAndLengthArguments,
26+
AST_POLYMORPHIC_SUPPORTED_TYPES(
27+
CallExpr, CXXConstructExpr,
28+
CXXUnresolvedConstructExpr, ObjCMessageExpr),
29+
unsigned, StringArgIndex, unsigned, LengthArgIndex) {
30+
if (StringArgIndex >= Node.getNumArgs() ||
31+
LengthArgIndex >= Node.getNumArgs()) {
32+
return false;
33+
}
34+
35+
const Expr *StringArgExpr =
36+
Node.getArg(StringArgIndex)->IgnoreParenImpCasts();
37+
const Expr *LengthArgExpr =
38+
Node.getArg(LengthArgIndex)->IgnoreParenImpCasts();
39+
40+
if (const auto *StringArg = dyn_cast<StringLiteral>(StringArgExpr)) {
41+
// Match an integer literal equal to the string length or a call to strlen.
42+
43+
static const auto Matcher = expr(anyOf(
44+
integerLiteral().bind("integer_literal_size"),
45+
callExpr(callee(functionDecl(hasName("strlen"))), argumentCountIs(1),
46+
hasArgument(0, stringLiteral().bind("strlen_arg")))));
47+
48+
if (!Matcher.matches(*LengthArgExpr, Finder, Builder)) {
49+
return false;
50+
}
51+
52+
return Builder->removeBindings(
53+
[&](const ast_matchers::internal::BoundNodesMap &Nodes) {
54+
const auto *IntegerLiteralSize =
55+
Nodes.getNodeAs<IntegerLiteral>("integer_literal_size");
56+
const auto *StrlenArg = Nodes.getNodeAs<StringLiteral>("strlen_arg");
57+
if (IntegerLiteralSize) {
58+
return IntegerLiteralSize->getValue().getZExtValue() !=
59+
StringArg->getLength();
60+
}
61+
return StrlenArg->getLength() != StringArg->getLength();
62+
});
63+
}
64+
65+
if (const auto *StringArg = dyn_cast<DeclRefExpr>(StringArgExpr)) {
66+
// Match a call to size() or length() on the same variable.
67+
68+
static const auto Matcher = cxxMemberCallExpr(
69+
on(declRefExpr(to(varDecl().bind("string_var_decl")))),
70+
callee(cxxMethodDecl(hasAnyName("size", "length"), isConst(),
71+
parameterCountIs(0))));
72+
73+
if (!Matcher.matches(*LengthArgExpr, Finder, Builder)) {
74+
return false;
75+
}
76+
77+
return Builder->removeBindings(
78+
[&](const ast_matchers::internal::BoundNodesMap &Nodes) {
79+
const auto *StringVarDecl =
80+
Nodes.getNodeAs<VarDecl>("string_var_decl");
81+
return StringVarDecl != StringArg->getDecl();
82+
});
83+
}
84+
85+
return false;
86+
}
87+
} // namespace
1988

2089
UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
2190
ClangTidyContext *Context)
@@ -43,7 +112,9 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
43112
callee(cxxMethodDecl(hasName("find")).bind("find_fun")),
44113
// ... on a class with a starts_with function.
45114
on(hasType(
46-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
115+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
116+
// Bind search expression.
117+
hasArgument(0, expr().bind("search_expr")));
47118

48119
const auto RFindExpr = cxxMemberCallExpr(
49120
// A method call with a second argument of zero...
@@ -52,15 +123,30 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
52123
callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
53124
// ... on a class with a starts_with function.
54125
on(hasType(
55-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
56-
57-
const auto FindOrRFindExpr =
58-
cxxMemberCallExpr(anyOf(FindExpr, RFindExpr)).bind("find_expr");
126+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
127+
// Bind search expression.
128+
hasArgument(0, expr().bind("search_expr")));
129+
130+
const auto CompareExpr = cxxMemberCallExpr(
131+
// A method call with a first argument of zero...
132+
hasArgument(0, ZeroLiteral),
133+
// ... named compare...
134+
callee(cxxMethodDecl(hasName("compare")).bind("find_fun")),
135+
// ... on a class with a starts_with function...
136+
on(hasType(
137+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
138+
// ... where the third argument is some string and the second its length.
139+
hasStringAndLengthArguments(2, 1),
140+
// Bind search expression.
141+
hasArgument(2, expr().bind("search_expr")));
59142

60143
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))
144+
// Match [=!]= with a zero on one side and (r?)find|compare on the other.
145+
binaryOperator(
146+
hasAnyOperatorName("==", "!="),
147+
hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr))
148+
.bind("find_expr"),
149+
ZeroLiteral))
64150
.bind("expr"),
65151
this);
66152
}
@@ -69,6 +155,7 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
69155
const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
70156
const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
71157
const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
158+
const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("search_expr");
72159
const auto *StartsWithFunction =
73160
Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
74161

@@ -79,13 +166,13 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
79166
const bool Neg = ComparisonExpr->getOpcode() == BO_NE;
80167

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

85-
// Remove possible zero second argument and ' [!=]= 0' suffix.
172+
// Remove possible arguments after search expression and ' [!=]= 0' suffix.
86173
Diagnostic << FixItHint::CreateReplacement(
87174
CharSourceRange::getTokenRange(
88-
Lexer::getLocForEndOfToken(FindExpr->getArg(0)->getEndLoc(), 0,
175+
Lexer::getLocForEndOfToken(SearchExpr->getEndLoc(), 0,
89176
*Result.SourceManager, getLangOpts()),
90177
ComparisonExpr->getEndLoc()),
91178
")");
@@ -94,11 +181,12 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
94181
Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
95182
ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
96183

97-
// Replace '(r?)find' with 'starts_with'.
184+
// Replace method name by 'starts_with'.
185+
// Remove possible arguments before search expression.
98186
Diagnostic << FixItHint::CreateReplacement(
99-
CharSourceRange::getTokenRange(FindExpr->getExprLoc(),
100-
FindExpr->getExprLoc()),
101-
StartsWithFunction->getName());
187+
CharSourceRange::getCharRange(FindExpr->getExprLoc(),
188+
SearchExpr->getBeginLoc()),
189+
(StartsWithFunction->getName() + "(").str());
102190

103191
// Add possible negation '!'.
104192
if (Neg) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
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.
16+
/// Checks for common roundabout ways to express `starts_with` and `ends_with`
17+
/// and suggests replacing with ``starts_with`` when the method is available.
1818
/// Notably, this will work with ``std::string`` and ``std::string_view``.
1919
///
2020
/// For the user-facing documentation see:

clang-tools-extra/docs/ReleaseNotes.rst

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

264+
- Improved :doc:`modernize-use-starts-ends-with
265+
<clang-tidy/checks/modernize/use-starts-ends-with>` check to also handle
266+
calls to ``compare`` method.
267+
264268
- Improved :doc:`modernize-use-using <clang-tidy/checks/modernize/use-using>`
265269
check by adding support for detection of typedefs declared on function level.
266270

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

Lines changed: 4 additions & 2 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,
6+
Checks for common roundabout ways to express `starts_with` and `ends_with` and
7+
suggests replacing with ``starts_with`` when the method is available. Notably,
88
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: 45 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,54 @@ 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, s.size(), s) == 0;
179+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
180+
// CHECK-FIXES: s.starts_with(s);
181+
182+
s.compare(0, s.length(), s) == 0;
183+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
184+
// CHECK-FIXES: s.starts_with(s);
185+
186+
0 != s.compare(0, sv.length(), sv);
187+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
188+
// CHECK-FIXES: s.starts_with(sv);
189+
190+
#define LENGTH(x) (x).length()
191+
s.compare(0, LENGTH(s), s) == 0;
192+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
193+
// CHECK-FIXES: s.starts_with(s);
194+
195+
s.compare(ZERO, LENGTH(s), s) == ZERO;
196+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
197+
// CHECK-FIXES: s.starts_with(s);
198+
199+
s.compare(ZERO, LENGTH(sv), sv) != 0;
200+
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
201+
// CHECK-FIXES: !s.starts_with(sv);
202+
161203
// Expressions that don't trigger the check are here.
162204
#define EQ(x, y) ((x) == (y))
163205
EQ(s.find("a"), 0);
164206

165207
#define DOTFIND(x, y) (x).find(y)
166208
DOTFIND(s, "a") == 0;
209+
210+
#define STARTS_WITH_COMPARE(x, y) (x).compare(0, (x).size(), (y))
211+
STARTS_WITH_COMPARE(s, s) == 0;
167212
}

0 commit comments

Comments
 (0)