Skip to content

Commit 8b2dc9a

Browse files
committed
[clang-tidy] Add readability-string-view-substr check
Add a new check that suggests using string_view::remove_prefix() and remove_suffix() instead of substr() when the intent is to remove characters from either end of a string_view.
1 parent b36f1c8 commit 8b2dc9a

File tree

7 files changed

+253
-0
lines changed

7 files changed

+253
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ add_clang_library(clangTidyReadabilityModule STATIC
5353
StaticAccessedThroughInstanceCheck.cpp
5454
StaticDefinitionInAnonymousNamespaceCheck.cpp
5555
StringCompareCheck.cpp
56+
StringViewSubstrCheck.cpp
5657
SuspiciousCallArgumentCheck.cpp
5758
UniqueptrDeleteReleaseCheck.cpp
5859
UppercaseLiteralSuffixCheck.cpp

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
#include "StaticAccessedThroughInstanceCheck.h"
5757
#include "StaticDefinitionInAnonymousNamespaceCheck.h"
5858
#include "StringCompareCheck.h"
59+
#include "StringViewSubstrCheck.h"
5960
#include "SuspiciousCallArgumentCheck.h"
6061
#include "UniqueptrDeleteReleaseCheck.h"
6162
#include "UppercaseLiteralSuffixCheck.h"
@@ -146,6 +147,8 @@ class ReadabilityModule : public ClangTidyModule {
146147
"readability-static-definition-in-anonymous-namespace");
147148
CheckFactories.registerCheck<StringCompareCheck>(
148149
"readability-string-compare");
150+
CheckFactories.registerCheck<StringViewSubstrCheck>(
151+
"readability-stringview-substr");
149152
CheckFactories.registerCheck<readability::NamedParameterCheck>(
150153
"readability-named-parameter");
151154
CheckFactories.registerCheck<NonConstParameterCheck>(
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===--- StringViewSubstrCheck.cpp - 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+
#include "StringViewSubstrCheck.h"
10+
#include "clang/AST/ASTContext.h"
11+
#include "clang/ASTMatchers/ASTMatchFinder.h"
12+
#include "clang/Lex/Lexer.h"
13+
14+
using namespace clang::ast_matchers;
15+
16+
namespace clang::tidy::readability {
17+
18+
void StringViewSubstrCheck::registerMatchers(MatchFinder *Finder) {
19+
const auto HasStringViewType = hasType(hasUnqualifiedDesugaredType(recordType(
20+
hasDeclaration(recordDecl(hasName("::std::basic_string_view"))))));
21+
22+
// Match assignment to string_view's substr
23+
Finder->addMatcher(
24+
cxxOperatorCallExpr(
25+
hasOverloadedOperatorName("="),
26+
hasArgument(0, expr(HasStringViewType).bind("target")),
27+
hasArgument(
28+
1, cxxMemberCallExpr(callee(memberExpr(hasDeclaration(
29+
cxxMethodDecl(hasName("substr"))))),
30+
on(expr(HasStringViewType).bind("source")))
31+
.bind("substr_call")))
32+
.bind("assignment"),
33+
this);
34+
}
35+
36+
void StringViewSubstrCheck::check(const MatchFinder::MatchResult &Result) {
37+
const auto *Assignment =
38+
Result.Nodes.getNodeAs<CXXOperatorCallExpr>("assignment");
39+
const auto *Target = Result.Nodes.getNodeAs<Expr>("target");
40+
const auto *Source = Result.Nodes.getNodeAs<Expr>("source");
41+
const auto *SubstrCall =
42+
Result.Nodes.getNodeAs<CXXMemberCallExpr>("substr_call");
43+
44+
if (!Assignment || !Target || !Source || !SubstrCall) {
45+
return;
46+
}
47+
48+
// Get the DeclRefExpr for the target and source to compare variables
49+
const auto *TargetDRE = dyn_cast<DeclRefExpr>(Target->IgnoreParenImpCasts());
50+
const auto *SourceDRE = dyn_cast<DeclRefExpr>(Source->IgnoreParenImpCasts());
51+
52+
// Only handle self-assignment cases
53+
if (!TargetDRE || !SourceDRE ||
54+
TargetDRE->getDecl() != SourceDRE->getDecl()) {
55+
return;
56+
}
57+
58+
const Expr *StartArg = SubstrCall->getArg(0);
59+
const Expr *LengthArg =
60+
SubstrCall->getNumArgs() > 1 ? SubstrCall->getArg(1) : nullptr;
61+
62+
// Get source text of first argument
63+
std::string StartText =
64+
Lexer::getSourceText(
65+
CharSourceRange::getTokenRange(StartArg->getSourceRange()),
66+
*Result.SourceManager, Result.Context->getLangOpts())
67+
.str();
68+
69+
// Case 1: Check for remove_prefix pattern - only when the second arg is
70+
// missing (uses npos)
71+
if (!LengthArg || isa<CXXDefaultArgExpr>(LengthArg)) {
72+
std::string Replacement = TargetDRE->getNameInfo().getAsString() +
73+
".remove_prefix(" + StartText + ")";
74+
diag(Assignment->getBeginLoc(), "prefer 'remove_prefix' over 'substr' for "
75+
"removing characters from the start")
76+
<< FixItHint::CreateReplacement(Assignment->getSourceRange(),
77+
Replacement);
78+
return;
79+
}
80+
81+
// Case 2: Check for remove_suffix pattern
82+
if (StartText == "0") {
83+
if (const auto *BinOp = dyn_cast<BinaryOperator>(LengthArg)) {
84+
if (BinOp->getOpcode() == BO_Sub) {
85+
const Expr *LHS = BinOp->getLHS();
86+
const Expr *RHS = BinOp->getRHS();
87+
88+
// Check if LHS is a length() call on the same string_view
89+
if (const auto *LengthCall = dyn_cast<CXXMemberCallExpr>(LHS)) {
90+
if (const auto *LengthMethod =
91+
dyn_cast<CXXMethodDecl>(LengthCall->getDirectCallee())) {
92+
if (LengthMethod->getName() == "length") {
93+
// Verify the length() call is on the same string_view
94+
const Expr *LengthObject =
95+
LengthCall->getImplicitObjectArgument();
96+
const auto *LengthDRE =
97+
dyn_cast<DeclRefExpr>(LengthObject->IgnoreParenImpCasts());
98+
99+
if (!LengthDRE || LengthDRE->getDecl() != TargetDRE->getDecl()) {
100+
return;
101+
}
102+
103+
// Must be a simple non-zero integer literal
104+
const auto *IL =
105+
dyn_cast<IntegerLiteral>(RHS->IgnoreParenImpCasts());
106+
if (!IL || IL->getValue() == 0) {
107+
return;
108+
}
109+
110+
std::string RHSText =
111+
Lexer::getSourceText(
112+
CharSourceRange::getTokenRange(RHS->getSourceRange()),
113+
*Result.SourceManager, Result.Context->getLangOpts())
114+
.str();
115+
116+
std::string Replacement = TargetDRE->getNameInfo().getAsString() +
117+
".remove_suffix(" + RHSText + ")";
118+
diag(Assignment->getBeginLoc(),
119+
"prefer 'remove_suffix' over 'substr' for removing "
120+
"characters from the end")
121+
<< FixItHint::CreateReplacement(Assignment->getSourceRange(),
122+
Replacement);
123+
return;
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
} // namespace clang::tidy::readability
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===--- StringViewSubstrCheck.h - clang-tidy---------------------*- C++
2+
//-*-===//
3+
//
4+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5+
// See https://llvm.org/LICENSE.txt for license information.
6+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7+
//
8+
//===----------------------------------------------------------------------===//
9+
10+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H
11+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H
12+
13+
#include "../ClangTidyCheck.h"
14+
15+
namespace clang::tidy::readability {
16+
17+
/// Finds string_view substr() calls that can be replaced with remove_prefix()
18+
/// or remove_suffix().
19+
///
20+
/// For the user-facing documentation see:
21+
/// https://clang.llvm.org/extra/clang-tidy/checks/readability/string-view-substr.html
22+
///
23+
/// The check matches two patterns:
24+
/// sv = sv.substr(N) -> sv.remove_prefix(N)
25+
/// sv = sv.substr(0, sv.length() - N) -> sv.remove_suffix(N)
26+
///
27+
/// These replacements make the intent clearer and are more efficient as they
28+
/// modify the string_view in place rather than creating a new one.
29+
class StringViewSubstrCheck : public ClangTidyCheck {
30+
public:
31+
StringViewSubstrCheck(StringRef Name, ClangTidyContext *Context)
32+
: ClangTidyCheck(Name, Context) {}
33+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
34+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
35+
};
36+
37+
} // namespace clang::tidy::readability
38+
39+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ New checks
136136
Gives warnings for tagged unions, where the number of tags is
137137
different from the number of data members inside the union.
138138

139+
- New :doc:`readability-string-view-substr
140+
<clang-tidy/checks/readability/string-view-substr>` check.
141+
142+
Finds ``std::string_view::substr()`` calls that can be replaced with clearer
143+
alternatives using ``remove_prefix()`` or ``remove_suffix()``. This makes the
144+
intent clearer and is more efficient as it modifies the string_view in place.
145+
139146
- New :doc:`portability-template-virtual-member-function
140147
<clang-tidy/checks/portability/template-virtual-member-function>` check.
141148

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.. title:: clang-tidy - readability-string-view-substr
2+
3+
readability-string-view-substr
4+
==============================
5+
6+
Finds ``string_view substr()`` calls that can be replaced with clearer alternatives
7+
using ``remove_prefix()`` or ``remove_suffix()``.
8+
9+
The check suggests the following transformations:
10+
11+
=========================================== =======================
12+
Expression Replacement
13+
=========================================== =======================
14+
``sv = sv.substr(n)`` ``sv.remove_prefix(n)``
15+
``sv = sv.substr(0, sv.length()-n)`` ``sv.remove_suffix(n)``
16+
=========================================== =======================
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// RUN: %check_clang_tidy %s readability-stringview-substr %t
2+
3+
namespace std {
4+
template <typename T>
5+
class basic_string_view {
6+
public:
7+
using size_type = unsigned long;
8+
static constexpr size_type npos = -1;
9+
10+
basic_string_view(const char*) {}
11+
basic_string_view substr(size_type pos, size_type count = npos) const { return *this; }
12+
void remove_prefix(size_type n) {}
13+
void remove_suffix(size_type n) {}
14+
size_type length() const { return 0; }
15+
16+
// Needed for assignment operator
17+
basic_string_view& operator=(const basic_string_view&) { return *this; }
18+
};
19+
20+
using string_view = basic_string_view<char>;
21+
}
22+
23+
void test() {
24+
std::string_view sv("test");
25+
std::string_view sv1("test");
26+
std::string_view sv2("other");
27+
28+
// Should match: Removing N characters from start
29+
sv = sv.substr(5);
30+
// CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_prefix' over 'substr' for removing characters from the start [readability-stringview-substr]
31+
// CHECK-FIXES: sv.remove_prefix(5)
32+
33+
// Should match: Removing N characters from end
34+
sv = sv.substr(0, sv.length() - 3);
35+
// CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr]
36+
// CHECK-FIXES: sv.remove_suffix(3)
37+
38+
// Should not match: Basic substring operations
39+
sv = sv.substr(0, 3); // No warning - taking first N characters
40+
sv = sv.substr(1, 3); // No warning - taking N characters from position 1
41+
42+
// Should not match: Operations on different string_views
43+
sv = sv2.substr(0, sv2.length() - 3); // No warning - not self-assignment
44+
sv = sv.substr(0, sv2.length() - 3); // No warning - length() from different string_view
45+
sv1 = sv1.substr(0, sv2.length() - 3); // No warning - length() from different string_view
46+
sv = sv1.substr(0, sv1.length() - 3); // No warning - not self-assignment
47+
48+
// Should not match: Not actually removing from end
49+
sv = sv.substr(0, sv.length()); // No warning - keeping entire string
50+
sv = sv.substr(0, sv.length() - 0); // No warning - subtracting zero
51+
52+
// Should not match: Complex expressions
53+
sv = sv.substr(0, sv.length() - (3 + 2)); // No warning - complex arithmetic
54+
sv = sv.substr(1 + 2, sv.length() - 3); // No warning - complex start position
55+
}

0 commit comments

Comments
 (0)