Skip to content

Commit 7a2ff84

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

File tree

8 files changed

+137
-21
lines changed

8 files changed

+137
-21
lines changed

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

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,49 @@
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+
const auto Matcher = expr(anyOf(
43+
integerLiteral(equals(StringArg->getLength())),
44+
callExpr(
45+
callee(functionDecl(hasName("strlen"))), argumentCountIs(1),
46+
hasArgument(0, stringLiteral(hasSize(StringArg->getLength()))))));
47+
return Matcher.matches(*LengthArgExpr, Finder, Builder);
48+
}
49+
50+
if (const auto *StringArg = dyn_cast<DeclRefExpr>(StringArgExpr)) {
51+
// Match a call to size() or length() on the same variable.
52+
const auto Matcher = cxxMemberCallExpr(
53+
on(declRefExpr(to(varDecl(equalsNode(StringArg->getDecl()))))),
54+
callee(cxxMethodDecl(hasAnyName("size", "length"), isConst(),
55+
parameterCountIs(0))));
56+
return Matcher.matches(*LengthArgExpr, Finder, Builder);
57+
}
58+
59+
return false;
60+
}
61+
} // namespace
1962

2063
UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
2164
ClangTidyContext *Context)
@@ -43,7 +86,9 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
4386
callee(cxxMethodDecl(hasName("find")).bind("find_fun")),
4487
// ... on a class with a starts_with function.
4588
on(hasType(
46-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
89+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
90+
// Bind search expression.
91+
hasArgument(0, expr().bind("search_expr")));
4792

4893
const auto RFindExpr = cxxMemberCallExpr(
4994
// A method call with a second argument of zero...
@@ -52,15 +97,30 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
5297
callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
5398
// ... on a class with a starts_with function.
5499
on(hasType(
55-
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
56-
57-
const auto FindOrRFindExpr =
58-
cxxMemberCallExpr(anyOf(FindExpr, RFindExpr)).bind("find_expr");
100+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
101+
// Bind search expression.
102+
hasArgument(0, expr().bind("search_expr")));
103+
104+
const auto CompareExpr = cxxMemberCallExpr(
105+
// A method call with a first argument of zero...
106+
hasArgument(0, ZeroLiteral),
107+
// ... named compare...
108+
callee(cxxMethodDecl(hasName("compare")).bind("find_fun")),
109+
// ... on a class with a starts_with function...
110+
on(hasType(
111+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
112+
// ... where the third argument is some string and the second its length.
113+
HasStringAndLengthArguments(2, 1),
114+
// Bind search expression.
115+
hasArgument(2, expr().bind("search_expr")));
59116

60117
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))
118+
// Match [=!]= with a zero on one side and (r?)find|compare on the other.
119+
binaryOperator(
120+
hasAnyOperatorName("==", "!="),
121+
hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr))
122+
.bind("find_expr"),
123+
ZeroLiteral))
64124
.bind("expr"),
65125
this);
66126
}
@@ -69,6 +129,7 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
69129
const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
70130
const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
71131
const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
132+
const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("search_expr");
72133
const auto *StartsWithFunction =
73134
Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
74135

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

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

85-
// Remove possible zero second argument and ' [!=]= 0' suffix.
146+
// Remove possible arguments after search expression and ' [!=]= 0' suffix.
86147
Diagnostic << FixItHint::CreateReplacement(
87148
CharSourceRange::getTokenRange(
88-
Lexer::getLocForEndOfToken(FindExpr->getArg(0)->getEndLoc(), 0,
149+
Lexer::getLocForEndOfToken(SearchExpr->getEndLoc(), 0,
89150
*Result.SourceManager, getLangOpts()),
90151
ComparisonExpr->getEndLoc()),
91152
")");
@@ -94,11 +155,12 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
94155
Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
95156
ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
96157

97-
// Replace '(r?)find' with 'starts_with'.
158+
// Replace method name by 'starts_with'.
159+
// Remove possible arguments before search expression.
98160
Diagnostic << FixItHint::CreateReplacement(
99-
CharSourceRange::getTokenRange(FindExpr->getExprLoc(),
100-
FindExpr->getExprLoc()),
101-
StartsWithFunction->getName());
161+
CharSourceRange::getCharRange(FindExpr->getExprLoc(),
162+
SearchExpr->getBeginLoc()),
163+
StartsWithFunction->getNameAsString() + "(");
102164

103165
// Add possible negation '!'.
104166
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
@@ -298,6 +298,10 @@ Changes in existing checks
298298
check by resolving fix-it overlaps in template code by disregarding implicit
299299
instances.
300300

301+
- Improved :doc:`modernize-use-starts-ends-with
302+
<clang-tidy/checks/modernize/use-starts-ends-with>` check to also handle
303+
cases using `compare()`.
304+
301305
Removed checks
302306
^^^^^^^^^^^^^^
303307

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)