Skip to content

Commit adafcce

Browse files
committed
[clang-tidy] Add modernize-substr-to-starts-with check
Adds a new check that finds calls to substr when its first argument is a zero-equivalent expression and can be replaced with starts_with() (introduced in C++20). This modernization improves code readability by making the intent clearer and can be more efficient as it avoids creating temporary strings. Converts patterns like: str.substr(0, 3) == "foo" -> str.starts_with("foo") str.substr(x-x, 3) == "foo" -> str.starts_with("foo") str.substr(zero, n) == prefix -> str.starts_with(prefix) "bar" == str.substr(i-i, 3) -> str.starts_with("bar") str.substr(0, n) != prefix -> !str.starts_with(prefix) The check: - Detects zero-equivalent expressions: * Direct zero literals (0) * Variables initialized to zero * Self-canceling expressions (x-x, i-i) - Only converts when length matches exactly for string literals - Supports both string literals and string variables - Handles both == and != operators
1 parent 5845688 commit adafcce

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ add_clang_library(clangTidyModernizeModule STATIC
4949
UseTransparentFunctorsCheck.cpp
5050
UseUncaughtExceptionsCheck.cpp
5151
UseUsingCheck.cpp
52+
SubstrToStartsWithCheck.cpp
5253

5354
LINK_LIBS
5455
clangTidy

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
#include "UseTransparentFunctorsCheck.h"
5151
#include "UseUncaughtExceptionsCheck.h"
5252
#include "UseUsingCheck.h"
53+
#include "SubstrToStartsWithCheck.h"
5354

5455
using namespace clang::ast_matchers;
5556

@@ -122,6 +123,8 @@ class ModernizeModule : public ClangTidyModule {
122123
CheckFactories.registerCheck<UseUncaughtExceptionsCheck>(
123124
"modernize-use-uncaught-exceptions");
124125
CheckFactories.registerCheck<UseUsingCheck>("modernize-use-using");
126+
CheckFactories.registerCheck<SubstrToStartsWithCheck>(
127+
"modernize-substr-to-starts-with");
125128
}
126129
};
127130

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//===--- SubstrToStartsWithCheck.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 "SubstrToStartsWithCheck.h"
10+
#include "clang/AST/ASTContext.h"
11+
#include "clang/ASTMatchers/ASTMatchFinder.h"
12+
#include "clang/Basic/SourceManager.h"
13+
#include "clang/Lex/Lexer.h"
14+
15+
using namespace clang::ast_matchers;
16+
17+
namespace clang::tidy::modernize {
18+
19+
void SubstrToStartsWithCheck::registerMatchers(MatchFinder *Finder) {
20+
auto isZeroExpr = expr(anyOf(
21+
integerLiteral(equals(0)),
22+
ignoringParenImpCasts(declRefExpr(
23+
to(varDecl(hasInitializer(integerLiteral(equals(0))))))),
24+
binaryOperator(hasOperatorName("-"), hasLHS(expr()), hasRHS(expr()))));
25+
26+
auto isStringLike = expr(anyOf(
27+
stringLiteral().bind("literal"),
28+
implicitCastExpr(hasSourceExpression(stringLiteral().bind("literal"))),
29+
declRefExpr(to(varDecl(hasType(qualType(hasDeclaration(
30+
namedDecl(hasAnyName("::std::string", "::std::basic_string")))))))).bind("strvar")));
31+
32+
auto isSubstrCall =
33+
cxxMemberCallExpr(
34+
callee(memberExpr(hasDeclaration(cxxMethodDecl(
35+
hasName("substr"),
36+
ofClass(hasAnyName("basic_string", "string", "u16string")))))),
37+
hasArgument(0, isZeroExpr),
38+
hasArgument(1, expr().bind("length")))
39+
.bind("substr");
40+
41+
Finder->addMatcher(
42+
binaryOperator(
43+
anyOf(hasOperatorName("=="), hasOperatorName("!=")),
44+
hasEitherOperand(isSubstrCall),
45+
hasEitherOperand(isStringLike),
46+
unless(hasType(isAnyCharacter())))
47+
.bind("comparison"),
48+
this);
49+
50+
Finder->addMatcher(
51+
cxxMemberCallExpr(
52+
callee(memberExpr(hasDeclaration(cxxMethodDecl(
53+
hasName("substr"),
54+
ofClass(hasAnyName("basic_string", "string", "u16string")))))),
55+
hasArgument(0, isZeroExpr),
56+
hasArgument(1, expr().bind("direct_length")))
57+
.bind("direct_substr"),
58+
this);
59+
}
60+
61+
void SubstrToStartsWithCheck::check(const MatchFinder::MatchResult &Result) {
62+
const auto *Comparison = Result.Nodes.getNodeAs<BinaryOperator>("comparison");
63+
64+
if (Comparison) {
65+
const auto *SubstrCall = Result.Nodes.getNodeAs<CXXMemberCallExpr>("substr");
66+
const auto *LengthArg = Result.Nodes.getNodeAs<Expr>("length");
67+
const auto *Literal = Result.Nodes.getNodeAs<StringLiteral>("literal");
68+
const auto *StrVar = Result.Nodes.getNodeAs<DeclRefExpr>("strvar");
69+
70+
if (!SubstrCall || !LengthArg || (!Literal && !StrVar))
71+
return;
72+
73+
std::string CompareStr;
74+
if (Literal) {
75+
CompareStr = Literal->getString().str();
76+
} else if (StrVar) {
77+
CompareStr = Lexer::getSourceText(
78+
CharSourceRange::getTokenRange(StrVar->getSourceRange()),
79+
*Result.SourceManager, Result.Context->getLangOpts())
80+
.str();
81+
}
82+
83+
if (Literal) {
84+
if (const auto *LengthLiteral = dyn_cast<IntegerLiteral>(LengthArg)) {
85+
if (LengthLiteral->getValue() != Literal->getLength())
86+
return;
87+
}
88+
}
89+
90+
std::string Replacement;
91+
if (Comparison->getOpcode() == BO_EQ) {
92+
Replacement = "starts_with(" + CompareStr + ")";
93+
} else { // BO_NE
94+
Replacement = "!starts_with(" + CompareStr + ")";
95+
}
96+
97+
diag(Comparison->getBeginLoc(),
98+
"use starts_with() instead of substring comparison")
99+
<< FixItHint::CreateReplacement(Comparison->getSourceRange(),
100+
Replacement);
101+
}
102+
}
103+
104+
} // namespace clang::tidy::modernize
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//===--- SubstrToStartsWithCheck.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_SUBSTRTOSTARTSWITHCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_SUBSTRTOSTARTSWITHCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::modernize {
15+
16+
/// Finds calls to substr(0, n) that can be replaced with starts_with().
17+
///
18+
/// For the user-facing documentation see:
19+
/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/substr-to-starts-with.html
20+
class SubstrToStartsWithCheck : public ClangTidyCheck {
21+
public:
22+
SubstrToStartsWithCheck(StringRef Name, ClangTidyContext *Context)
23+
: ClangTidyCheck(Name, Context) {}
24+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
25+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
26+
};
27+
28+
} // namespace clang::tidy::modernize
29+
30+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_SUBSTRTOSTARTSWITHCHECK_H
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
modernize-substr-to-starts-with
2+
==============================
3+
4+
Finds calls to ``substr(0, n)`` that can be replaced with ``starts_with()`` (introduced in C++20).
5+
This makes the code's intent clearer and can be more efficient as it avoids creating temporary strings.
6+
7+
For example:
8+
9+
.. code-block:: c++
10+
11+
str.substr(0, 3) == "foo" // before
12+
str.starts_with("foo") // after
13+
14+
"bar" == str.substr(0, 3) // before
15+
str.starts_with("bar") // after
16+
17+
str.substr(0, n) == prefix // before
18+
str.starts_with(prefix) // after
19+
20+
The check handles various ways of expressing zero as the start index:
21+
22+
.. code-block:: c++
23+
24+
const int zero = 0;
25+
str.substr(zero, n) == prefix // converted
26+
str.substr(x - x, n) == prefix // converted
27+
28+
The check will only convert cases where:
29+
* The substr call starts at index 0 (or equivalent)
30+
* When comparing with string literals, the length matches exactly
31+
* The comparison is with == or !=
32+
33+
.. code-block:: c++
34+
35+
auto prefix = str.substr(0, n); // warns about possible use of starts_with
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// RUN: %check_clang_tidy %s modernize-substr-to-starts-with %t -- -std=c++20 -stdlib=libc++
2+
3+
#include <string>
4+
#include <string_view>
5+
6+
void test() {
7+
std::string str = "hello world";
8+
if (str.substr(0, 5) == "hello") {}
9+
// CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use starts_with() instead of substring comparison
10+
// CHECK-FIXES: if (str.starts_with("hello")) {}
11+
12+
if ("hello" == str.substr(0, 5)) {}
13+
// CHECK-MESSAGES: :[[@LINE-1]]:19: warning: use starts_with() instead of substring comparison
14+
// CHECK-FIXES: if (str.starts_with("hello")) {}
15+
16+
bool b = str.substr(0, 5) != "hello";
17+
// CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use starts_with() instead of substring comparison
18+
// CHECK-FIXES: bool b = !str.starts_with("hello");
19+
20+
// Variable length and string refs
21+
std::string prefix = "hello";
22+
size_t len = 5;
23+
if (str.substr(0, len) == prefix) {}
24+
// CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use starts_with() instead of substring comparison
25+
// CHECK-FIXES: if (str.starts_with(prefix)) {}
26+
27+
// Various zero expressions
28+
const int zero = 0;
29+
int i = 0;
30+
if (str.substr(zero, 5) == "hello") {}
31+
// CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use starts_with() instead of substring comparison
32+
// CHECK-FIXES: if (str.starts_with("hello")) {}
33+
34+
if (str.substr(i-i, 5) == "hello") {}
35+
// CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use starts_with() instead of substring comparison
36+
// CHECK-FIXES: if (str.starts_with("hello")) {}
37+
38+
// Should not convert these
39+
if (str.substr(1, 5) == "hello") {} // Non-zero start
40+
if (str.substr(0, 4) == "hello") {} // Length mismatch
41+
if (str.substr(0, 6) == "hello") {} // Length mismatch
42+
}

0 commit comments

Comments
 (0)