Skip to content

Commit bc8cff1

Browse files
authored
[clang-tidy] Add new modernize-use-starts-ends-with check (#72385)
Make a modernize version of abseil-string-find-startswith using the available C++20 `std::string::starts_with` and `std::string_view::starts_with`. Following up from #72283.
1 parent 2b7191c commit bc8cff1

File tree

14 files changed

+374
-6
lines changed

14 files changed

+374
-6
lines changed

clang-tools-extra/clang-tidy/abseil/StringFindStartswithCheck.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ namespace clang::tidy::abseil {
2121

2222
// Find string.find(...) == 0 comparisons and suggest replacing with StartsWith.
2323
// FIXME(niko): Add similar check for EndsWith
24-
// FIXME(niko): Add equivalent modernize checks for C++20's std::starts_With
2524
class StringFindStartswithCheck : public ClangTidyCheck {
2625
public:
2726
using ClangTidyCheck::ClangTidyCheck;
@@ -31,6 +30,10 @@ class StringFindStartswithCheck : public ClangTidyCheck {
3130
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
3231
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
3332
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
33+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
34+
// Prefer modernize-use-starts-ends-with when C++20 is available.
35+
return LangOpts.CPlusPlus && !LangOpts.CPlusPlus20;
36+
}
3437

3538
private:
3639
const std::vector<StringRef> StringLikeClasses;

clang-tools-extra/clang-tidy/modernize/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ add_clang_library(clangTidyModernizeModule
3838
UseNoexceptCheck.cpp
3939
UseNullptrCheck.cpp
4040
UseOverrideCheck.cpp
41+
UseStartsEndsWithCheck.cpp
4142
UseStdPrintCheck.cpp
4243
UseTrailingReturnTypeCheck.cpp
4344
UseTransparentFunctorsCheck.cpp

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "UseNoexceptCheck.h"
4040
#include "UseNullptrCheck.h"
4141
#include "UseOverrideCheck.h"
42+
#include "UseStartsEndsWithCheck.h"
4243
#include "UseStdPrintCheck.h"
4344
#include "UseTrailingReturnTypeCheck.h"
4445
#include "UseTransparentFunctorsCheck.h"
@@ -66,6 +67,8 @@ class ModernizeModule : public ClangTidyModule {
6667
CheckFactories.registerCheck<MakeSharedCheck>("modernize-make-shared");
6768
CheckFactories.registerCheck<MakeUniqueCheck>("modernize-make-unique");
6869
CheckFactories.registerCheck<PassByValueCheck>("modernize-pass-by-value");
70+
CheckFactories.registerCheck<UseStartsEndsWithCheck>(
71+
"modernize-use-starts-ends-with");
6972
CheckFactories.registerCheck<UseStdPrintCheck>("modernize-use-std-print");
7073
CheckFactories.registerCheck<RawStringLiteralCheck>(
7174
"modernize-raw-string-literal");
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//===--- UseStartsEndsWithCheck.cpp - clang-tidy --------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "UseStartsEndsWithCheck.h"
10+
11+
#include "../utils/OptionsUtils.h"
12+
#include "clang/Lex/Lexer.h"
13+
14+
#include <string>
15+
16+
using namespace clang::ast_matchers;
17+
18+
namespace clang::tidy::modernize {
19+
20+
UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
21+
ClangTidyContext *Context)
22+
: ClangTidyCheck(Name, Context) {}
23+
24+
void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
25+
const auto ZeroLiteral = integerLiteral(equals(0));
26+
const auto HasStartsWithMethodWithName = [](const std::string &Name) {
27+
return hasMethod(
28+
cxxMethodDecl(hasName(Name), isConst(), parameterCountIs(1))
29+
.bind("starts_with_fun"));
30+
};
31+
const auto HasStartsWithMethod =
32+
anyOf(HasStartsWithMethodWithName("starts_with"),
33+
HasStartsWithMethodWithName("startsWith"),
34+
HasStartsWithMethodWithName("startswith"));
35+
const auto ClassWithStartsWithFunction = cxxRecordDecl(anyOf(
36+
HasStartsWithMethod, hasAnyBase(hasType(hasCanonicalType(hasDeclaration(
37+
cxxRecordDecl(HasStartsWithMethod)))))));
38+
39+
const auto FindExpr = cxxMemberCallExpr(
40+
// A method call with no second argument or the second argument is zero...
41+
anyOf(argumentCountIs(1), hasArgument(1, ZeroLiteral)),
42+
// ... named find...
43+
callee(cxxMethodDecl(hasName("find")).bind("find_fun")),
44+
// ... on a class with a starts_with function.
45+
on(hasType(
46+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
47+
48+
const auto RFindExpr = cxxMemberCallExpr(
49+
// A method call with a second argument of zero...
50+
hasArgument(1, ZeroLiteral),
51+
// ... named rfind...
52+
callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
53+
// ... on a class with a starts_with function.
54+
on(hasType(
55+
hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))));
56+
57+
const auto FindOrRFindExpr =
58+
cxxMemberCallExpr(anyOf(FindExpr, RFindExpr)).bind("find_expr");
59+
60+
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))
64+
.bind("expr"),
65+
this);
66+
}
67+
68+
void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
69+
const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
70+
const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
71+
const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
72+
const auto *StartsWithFunction =
73+
Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
74+
75+
if (ComparisonExpr->getBeginLoc().isMacroID()) {
76+
return;
77+
}
78+
79+
const bool Neg = ComparisonExpr->getOpcode() == BO_NE;
80+
81+
auto Diagnostic =
82+
diag(FindExpr->getBeginLoc(), "use %0 instead of %1() %select{==|!=}2 0")
83+
<< StartsWithFunction->getName() << FindFun->getName() << Neg;
84+
85+
// Remove possible zero second argument and ' [!=]= 0' suffix.
86+
Diagnostic << FixItHint::CreateReplacement(
87+
CharSourceRange::getTokenRange(
88+
Lexer::getLocForEndOfToken(FindExpr->getArg(0)->getEndLoc(), 0,
89+
*Result.SourceManager, getLangOpts()),
90+
ComparisonExpr->getEndLoc()),
91+
")");
92+
93+
// Remove possible '0 [!=]= ' prefix.
94+
Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
95+
ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
96+
97+
// Replace '(r?)find' with 'starts_with'.
98+
Diagnostic << FixItHint::CreateReplacement(
99+
CharSourceRange::getTokenRange(FindExpr->getExprLoc(),
100+
FindExpr->getExprLoc()),
101+
StartsWithFunction->getName());
102+
103+
// Add possible negation '!'.
104+
if (Neg) {
105+
Diagnostic << FixItHint::CreateInsertion(FindExpr->getBeginLoc(), "!");
106+
}
107+
}
108+
109+
} // namespace clang::tidy::modernize
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===--- UseStartsEndsWithCheck.h - clang-tidy ------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTARTSENDSWITHCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTARTSENDSWITHCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::modernize {
15+
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``.
19+
///
20+
/// For the user-facing documentation see:
21+
/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/use-starts-ends-with.html
22+
class UseStartsEndsWithCheck : public ClangTidyCheck {
23+
public:
24+
UseStartsEndsWithCheck(StringRef Name, ClangTidyContext *Context);
25+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
26+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
27+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28+
return LangOpts.CPlusPlus;
29+
}
30+
std::optional<TraversalKind> getCheckTraversalKind() const override {
31+
return TK_IgnoreUnlessSpelledInSource;
32+
}
33+
};
34+
35+
} // namespace clang::tidy::modernize
36+
37+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTARTSENDSWITHCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ New checks
186186

187187
Replace ``enable_if`` with C++20 requires clauses.
188188

189+
- New :doc:`modernize-use-starts-ends-with
190+
<clang-tidy/checks/modernize/use-starts-ends-with>` check.
191+
192+
Checks whether a ``find`` or ``rfind`` result is compared with 0 and suggests
193+
replacing with ``starts_with`` when the method exists in the class. Notably,
194+
this will work with ``std::string`` and ``std::string_view``.
195+
189196
- New :doc:`performance-enum-size
190197
<clang-tidy/checks/performance/enum-size>` check.
191198

clang-tools-extra/docs/clang-tidy/checks/abseil/string-find-startswith.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ corresponding ``std::string_view`` methods) result is compared with 0, and
88
suggests replacing with ``absl::StartsWith()``. This is both a readability and
99
performance issue.
1010

11+
``starts_with`` was added as a built-in function on those types in C++20. If
12+
available, prefer enabling :doc:`modernize-use-starts-ends-with
13+
<../modernize/use-starts-ends-with>` instead of this check.
14+
1115
.. code-block:: c++
1216

1317
string s = "...";

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ Clang-Tidy Checks
292292
:doc:`modernize-use-noexcept <modernize/use-noexcept>`, "Yes"
293293
:doc:`modernize-use-nullptr <modernize/use-nullptr>`, "Yes"
294294
:doc:`modernize-use-override <modernize/use-override>`, "Yes"
295+
:doc:`modernize-use-starts-ends-with <modernize/use-starts-ends-with>`, "Yes"
295296
:doc:`modernize-use-std-print <modernize/use-std-print>`, "Yes"
296297
:doc:`modernize-use-trailing-return-type <modernize/use-trailing-return-type>`, "Yes"
297298
:doc:`modernize-use-transparent-functors <modernize/use-transparent-functors>`, "Yes"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.. title:: clang-tidy - modernize-use-starts-ends-with
2+
3+
modernize-use-starts-ends-with
4+
==============================
5+
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``.
9+
10+
.. code-block:: c++
11+
12+
std::string s = "...";
13+
if (s.find("prefix") == 0) { /* do something */ }
14+
if (s.rfind("prefix", 0) == 0) { /* do something */ }
15+
16+
becomes
17+
18+
.. code-block:: c++
19+
20+
std::string s = "...";
21+
if (s.starts_with("prefix")) { /* do something */ }
22+
if (s.starts_with("prefix")) { /* do something */ }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
typedef __PTRDIFF_TYPE__ ptrdiff_t;
1313
typedef __SIZE_TYPE__ size_t;
1414

15-
#endif _STDDEF_H_
15+
#endif // _STDDEF_H_

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ typedef unsigned __INT32_TYPE__ char32;
1010
namespace std {
1111
template <typename T>
1212
class allocator {};
13+
1314
template <typename T>
1415
class char_traits {};
16+
17+
template <typename C, typename T = char_traits<C>>
18+
struct basic_string_view;
19+
1520
template <typename C, typename T = char_traits<C>, typename A = allocator<C>>
1621
struct basic_string {
1722
typedef size_t size_type;
@@ -52,6 +57,10 @@ struct basic_string {
5257
_Type& insert(size_type pos, const C* s);
5358
_Type& insert(size_type pos, const C* s, size_type n);
5459

60+
constexpr bool starts_with(std::basic_string_view<C, T> sv) const noexcept;
61+
constexpr bool starts_with(C ch) const noexcept;
62+
constexpr bool starts_with(const C* s) const;
63+
5564
_Type& operator[](size_type);
5665
const _Type& operator[](size_type) const;
5766

@@ -68,7 +77,7 @@ typedef basic_string<wchar_t> wstring;
6877
typedef basic_string<char16> u16string;
6978
typedef basic_string<char32> u32string;
7079

71-
template <typename C, typename T = char_traits<C>>
80+
template <typename C, typename T>
7281
struct basic_string_view {
7382
typedef size_t size_type;
7483
typedef basic_string_view<C, T> _Type;
@@ -86,8 +95,13 @@ struct basic_string_view {
8695
size_type rfind(const C* s, size_type pos, size_type count) const;
8796
size_type rfind(const C* s, size_type pos = npos) const;
8897

98+
constexpr bool starts_with(basic_string_view sv) const noexcept;
99+
constexpr bool starts_with(C ch) const noexcept;
100+
constexpr bool starts_with(const C* s) const;
101+
89102
static constexpr size_t npos = -1;
90103
};
104+
91105
typedef basic_string_view<char> string_view;
92106
typedef basic_string_view<wchar_t> wstring_view;
93107
typedef basic_string_view<char16> u16string_view;

clang-tools-extra/test/clang-tidy/checkers/abseil/string-find-startswith.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %check_clang_tidy %s abseil-string-find-startswith %t -- \
1+
// RUN: %check_clang_tidy -std=c++17 %s abseil-string-find-startswith %t -- \
22
// RUN: -config="{CheckOptions: \
33
// RUN: {abseil-string-find-startswith.StringLikeClasses: \
44
// RUN: '::std::basic_string;::std::basic_string_view;::basic_string'}}" \

0 commit comments

Comments
 (0)