[clang] [clang-tools-extra] [clang][ASTMatcher] Fix execution order of hasOperands submatchers (PR #104148)
Nicolas van Kempen via cfe-commits
cfe-commits at lists.llvm.org
Wed Aug 14 11:48:06 PDT 2024
https://github.com/nicovank updated https://github.com/llvm/llvm-project/pull/104148
>From 6429dd6935343b343ce3082c13e772425caea15a Mon Sep 17 00:00:00 2001
From: Nicolas van Kempen <nvankemp at gmail.com>
Date: Wed, 14 Aug 2024 14:47:52 -0400
Subject: [PATCH 1/2] [clang][ASTMatcher] Fix execution order of hasOperands
submatchers
`hasOperands` does not always execute matchers in the order they are written.
This can cause issue in code using bindings when one operand matcher is relying
on a binding set by the other. With this change, the first matcher present in
the code is always executed first and any binding it sets are available to the
second matcher.
Simple example with current version (1 match) and new version (2 matches):
```
> cat tmp.cpp
int a = 13;
int b = ((int) a) - a;
int c = a - ((int) a);
> clang-query tmp.cpp
clang-query> set traversal IgnoreUnlessSpelledInSource
clang-query> m binaryOperator(hasOperands(cStyleCastExpr(has(declRefExpr(hasDeclaration(valueDecl().bind("d"))))), declRefExpr(hasDeclaration(valueDecl(equalsBoundNode("d"))))))
Match #1:
tmp.cpp:1:1: note: "d" binds here
int a = 13;
^~~~~~~~~~
tmp.cpp:2:9: note: "root" binds here
int b = ((int)a) - a;
^~~~~~~~~~~~
1 match.
> ./build/bin/clang-query tmp.cpp
clang-query> set traversal IgnoreUnlessSpelledInSource
clang-query> m binaryOperator(hasOperands(cStyleCastExpr(has(declRefExpr(hasDeclaration(valueDecl().bind("d"))))), declRefExpr(hasDeclaration(valueDecl(equalsBoundNode("d"))))))
Match #1:
tmp.cpp:1:1: note: "d" binds here
1 | int a = 13;
| ^~~~~~~~~~
tmp.cpp:2:9: note: "root" binds here
2 | int b = ((int)a) - a;
| ^~~~~~~~~~~~
Match #2:
tmp.cpp:1:1: note: "d" binds here
1 | int a = 13;
| ^~~~~~~~~~
tmp.cpp:3:9: note: "root" binds here
3 | int c = a - ((int)a);
| ^~~~~~~~~~~~
2 matches.
```
---
clang/include/clang/ASTMatchers/ASTMatchers.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/include/clang/ASTMatchers/ASTMatchers.h b/clang/include/clang/ASTMatchers/ASTMatchers.h
index ca44c3ee085654..f1c72efc238784 100644
--- a/clang/include/clang/ASTMatchers/ASTMatchers.h
+++ b/clang/include/clang/ASTMatchers/ASTMatchers.h
@@ -6027,7 +6027,7 @@ AST_POLYMORPHIC_MATCHER_P2(
internal::Matcher<Expr>, Matcher1, internal::Matcher<Expr>, Matcher2) {
return internal::VariadicDynCastAllOfMatcher<Stmt, NodeType>()(
anyOf(allOf(hasLHS(Matcher1), hasRHS(Matcher2)),
- allOf(hasLHS(Matcher2), hasRHS(Matcher1))))
+ allOf(hasRHS(Matcher1), hasLHS(Matcher2))))
.matches(Node, Finder, Builder);
}
>From 20b4cbaba593ef82dfaa74f468d78e4f162e544e Mon Sep 17 00:00:00 2001
From: Nicolas van Kempen <nvankemp at gmail.com>
Date: Wed, 14 Aug 2024 14:47:52 -0400
Subject: [PATCH 2/2] [clang-tidy][modernize-use-starts-ends-with] Add support
for two ends_with patterns
Add support for the following two patterns:
```
haystack.compare(haystack.length() - needle.length(), needle.length(), needle) == 0;
haystack.rfind(needle) == (haystack.size() - needle.size());
```
---
.../modernize/UseStartsEndsWithCheck.cpp | 222 +++++++++++-------
.../clang-tidy/checkers/Inputs/Headers/string | 8 +
.../modernize/use-starts-ends-with.cpp | 57 +++++
3 files changed, 196 insertions(+), 91 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp
index 89ee45faecd7f3..11c8d60933aacf 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp
@@ -8,6 +8,7 @@
#include "UseStartsEndsWithCheck.h"
+#include "../utils/ASTUtils.h"
#include "../utils/OptionsUtils.h"
#include "clang/Lex/Lexer.h"
@@ -16,6 +17,59 @@
using namespace clang::ast_matchers;
namespace clang::tidy::modernize {
+struct NotLengthExprForStringNode {
+ NotLengthExprForStringNode(std::string ID, DynTypedNode Node,
+ ASTContext *Context)
+ : ID(std::move(ID)), Node(std::move(Node)), Context(Context) {}
+ bool operator()(const internal::BoundNodesMap &Nodes) const {
+ if (const auto *StringLiteralNode = Nodes.getNodeAs<StringLiteral>(ID)) {
+ if (const auto *IntegerLiteralSizeNode = Node.get<IntegerLiteral>()) {
+ return StringLiteralNode->getLength() !=
+ IntegerLiteralSizeNode->getValue().getZExtValue();
+ }
+
+ if (const auto *StrlenNode = Node.get<CallExpr>()) {
+ if (StrlenNode->getDirectCallee()->getName() != "strlen" ||
+ StrlenNode->getNumArgs() != 1) {
+ return true;
+ }
+
+ if (const auto *StrlenArgNode = dyn_cast<StringLiteral>(
+ StrlenNode->getArg(0)->IgnoreParenImpCasts())) {
+ return StrlenArgNode->getLength() != StringLiteralNode->getLength();
+ }
+ }
+ } else if (const auto *ExprNode = Nodes.getNodeAs<Expr>(ID)) {
+ if (const auto *MemberCallNode = Node.get<CXXMemberCallExpr>()) {
+ const CXXMethodDecl *MethodDeclNode = MemberCallNode->getMethodDecl();
+ const StringRef Name = MethodDeclNode->getName();
+ if (!MethodDeclNode->isConst() || MethodDeclNode->getNumParams() != 0 ||
+ (Name != "size" && Name != "length")) {
+ return true;
+ }
+
+ if (const auto *OnNode =
+ dyn_cast<Expr>(MemberCallNode->getImplicitObjectArgument())) {
+ return !utils::areStatementsIdentical(OnNode->IgnoreParenImpCasts(),
+ ExprNode->IgnoreParenImpCasts(),
+ *Context);
+ }
+ }
+ }
+
+ return true;
+ }
+
+private:
+ std::string ID;
+ DynTypedNode Node;
+ ASTContext *Context;
+};
+
+AST_MATCHER_P(Expr, lengthExprForStringNode, std::string, ID) {
+ return Builder->removeBindings(NotLengthExprForStringNode(
+ ID, DynTypedNode::create(Node), &(Finder->getASTContext())));
+}
UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
ClangTidyContext *Context)
@@ -23,6 +77,7 @@ UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
const auto ZeroLiteral = integerLiteral(equals(0));
+
const auto HasStartsWithMethodWithName = [](const std::string &Name) {
return hasMethod(
cxxMethodDecl(hasName(Name), isConst(), parameterCountIs(1))
@@ -32,119 +87,104 @@ void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
anyOf(HasStartsWithMethodWithName("starts_with"),
HasStartsWithMethodWithName("startsWith"),
HasStartsWithMethodWithName("startswith"));
- const auto ClassWithStartsWithFunction = cxxRecordDecl(anyOf(
- HasStartsWithMethod, hasAnyBase(hasType(hasCanonicalType(hasDeclaration(
- cxxRecordDecl(HasStartsWithMethod)))))));
+ const auto OnClassWithStartsWithFunction =
+ on(hasType(hasCanonicalType(hasDeclaration(cxxRecordDecl(
+ anyOf(HasStartsWithMethod,
+ hasAnyBase(hasType(hasCanonicalType(
+ hasDeclaration(cxxRecordDecl(HasStartsWithMethod)))))))))));
+ const auto HasEndsWithMethodWithName = [](const std::string &Name) {
+ return hasMethod(
+ cxxMethodDecl(hasName(Name), isConst(), parameterCountIs(1))
+ .bind("ends_with_fun"));
+ };
+ const auto HasEndsWithMethod = anyOf(HasEndsWithMethodWithName("ends_with"),
+ HasEndsWithMethodWithName("endsWith"),
+ HasEndsWithMethodWithName("endswith"));
+ const auto OnClassWithEndsWithFunction =
+ on(expr(hasType(hasCanonicalType(hasDeclaration(cxxRecordDecl(
+ anyOf(HasEndsWithMethod,
+ hasAnyBase(hasType(hasCanonicalType(hasDeclaration(
+ cxxRecordDecl(HasEndsWithMethod)))))))))))
+ .bind("haystack"));
+
+ // Case 1: X.find(Y) [!=]= 0 -> starts_with.
const auto FindExpr = cxxMemberCallExpr(
- // A method call with no second argument or the second argument is zero...
anyOf(argumentCountIs(1), hasArgument(1, ZeroLiteral)),
- // ... named find...
callee(cxxMethodDecl(hasName("find")).bind("find_fun")),
- // ... on a class with a starts_with function.
- on(hasType(
- hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
- // Bind search expression.
- hasArgument(0, expr().bind("search_expr")));
+ OnClassWithStartsWithFunction, hasArgument(0, expr().bind("needle")));
+ // Case 2: X.rfind(Y, 0) [!=]= 0 -> starts_with.
const auto RFindExpr = cxxMemberCallExpr(
- // A method call with a second argument of zero...
hasArgument(1, ZeroLiteral),
- // ... named rfind...
callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
- // ... on a class with a starts_with function.
- on(hasType(
- hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
- // Bind search expression.
- hasArgument(0, expr().bind("search_expr")));
-
- // Match a string literal and an integer or strlen() call matching the length.
- const auto HasStringLiteralAndLengthArgs = [](const auto StringArgIndex,
- const auto LengthArgIndex) {
- return allOf(
- hasArgument(StringArgIndex, stringLiteral().bind("string_literal_arg")),
- hasArgument(LengthArgIndex,
- anyOf(integerLiteral().bind("integer_literal_size_arg"),
- callExpr(callee(functionDecl(parameterCountIs(1),
- hasName("strlen"))),
- hasArgument(0, stringLiteral().bind(
- "strlen_arg"))))));
- };
-
- // Match a string variable and a call to length() or size().
- const auto HasStringVariableAndSizeCallArgs = [](const auto StringArgIndex,
- const auto LengthArgIndex) {
- return allOf(
- hasArgument(StringArgIndex, declRefExpr(hasDeclaration(
- decl().bind("string_var_decl")))),
- hasArgument(LengthArgIndex,
- cxxMemberCallExpr(
- callee(cxxMethodDecl(isConst(), parameterCountIs(0),
- hasAnyName("size", "length"))),
- on(declRefExpr(
- to(decl(equalsBoundNode("string_var_decl"))))))));
- };
-
- // Match either one of the two cases above.
- const auto HasStringAndLengthArgs =
- [HasStringLiteralAndLengthArgs, HasStringVariableAndSizeCallArgs](
- const auto StringArgIndex, const auto LengthArgIndex) {
- return anyOf(
- HasStringLiteralAndLengthArgs(StringArgIndex, LengthArgIndex),
- HasStringVariableAndSizeCallArgs(StringArgIndex, LengthArgIndex));
- };
+ OnClassWithStartsWithFunction, hasArgument(0, expr().bind("needle")));
+ // Case 3: X.compare(0, LEN(Y), Y) [!=]= 0 -> starts_with.
const auto CompareExpr = cxxMemberCallExpr(
- // A method call with three arguments...
- argumentCountIs(3),
- // ... where the first argument is zero...
- hasArgument(0, ZeroLiteral),
- // ... named compare...
+ argumentCountIs(3), hasArgument(0, ZeroLiteral),
callee(cxxMethodDecl(hasName("compare")).bind("find_fun")),
- // ... on a class with a starts_with function...
- on(hasType(
- hasCanonicalType(hasDeclaration(ClassWithStartsWithFunction)))),
- // ... where the third argument is some string and the second a length.
- HasStringAndLengthArgs(2, 1),
- // Bind search expression.
- hasArgument(2, expr().bind("search_expr")));
+ OnClassWithStartsWithFunction, hasArgument(2, expr().bind("needle")),
+ hasArgument(1, lengthExprForStringNode("needle")));
+ // Case 4: X.compare(LEN(X) - LEN(Y), LEN(Y), Y) [!=]= 0 -> ends_with.
+ const auto CompareEndsWithExpr = cxxMemberCallExpr(
+ argumentCountIs(3),
+ callee(cxxMethodDecl(hasName("compare")).bind("find_fun")),
+ OnClassWithEndsWithFunction, hasArgument(2, expr().bind("needle")),
+ hasArgument(1, lengthExprForStringNode("needle")),
+ hasArgument(0,
+ binaryOperator(hasOperatorName("-"),
+ hasLHS(lengthExprForStringNode("haystack")),
+ hasRHS(lengthExprForStringNode("needle")))));
+
+ // All cases comparing to 0.
Finder->addMatcher(
- // Match [=!]= with a zero on one side and (r?)find|compare on the other.
binaryOperator(
hasAnyOperatorName("==", "!="),
- hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr))
+ hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr,
+ CompareEndsWithExpr))
.bind("find_expr"),
ZeroLiteral))
.bind("expr"),
this);
+
+ // Case 5: X.rfind(Y) [!=]= LEN(X) - LEN(Y) -> ends_with.
+ Finder->addMatcher(
+ binaryOperator(
+ hasAnyOperatorName("==", "!="),
+ hasOperands(
+ cxxMemberCallExpr(
+ anyOf(
+ argumentCountIs(1),
+ allOf(argumentCountIs(2),
+ hasArgument(
+ 1,
+ anyOf(declRefExpr(to(varDecl(hasName("npos")))),
+ memberExpr(member(hasName("npos"))))))),
+ callee(cxxMethodDecl(hasName("rfind")).bind("find_fun")),
+ OnClassWithEndsWithFunction,
+ hasArgument(0, expr().bind("needle")))
+ .bind("find_expr"),
+ binaryOperator(hasOperatorName("-"),
+ hasLHS(lengthExprForStringNode("haystack")),
+ hasRHS(lengthExprForStringNode("needle")))))
+ .bind("expr"),
+ this);
}
void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
- const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("search_expr");
+ const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("needle");
const auto *StartsWithFunction =
Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
-
- const auto *StringLiteralArg =
- Result.Nodes.getNodeAs<StringLiteral>("string_literal_arg");
- const auto *IntegerLiteralSizeArg =
- Result.Nodes.getNodeAs<IntegerLiteral>("integer_literal_size_arg");
- const auto *StrlenArg = Result.Nodes.getNodeAs<StringLiteral>("strlen_arg");
-
- // Filter out compare cases where the length does not match string literal.
- if (StringLiteralArg && IntegerLiteralSizeArg &&
- StringLiteralArg->getLength() !=
- IntegerLiteralSizeArg->getValue().getZExtValue()) {
- return;
- }
-
- if (StringLiteralArg && StrlenArg &&
- StringLiteralArg->getLength() != StrlenArg->getLength()) {
- return;
- }
+ const auto *EndsWithFunction =
+ Result.Nodes.getNodeAs<CXXMethodDecl>("ends_with_fun");
+ assert(bool(StartsWithFunction) != bool(EndsWithFunction));
+ const CXXMethodDecl *ReplacementFunction =
+ StartsWithFunction ? StartsWithFunction : EndsWithFunction;
if (ComparisonExpr->getBeginLoc().isMacroID()) {
return;
@@ -154,9 +194,9 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
auto Diagnostic =
diag(FindExpr->getExprLoc(), "use %0 instead of %1() %select{==|!=}2 0")
- << StartsWithFunction->getName() << FindFun->getName() << Neg;
+ << ReplacementFunction->getName() << FindFun->getName() << Neg;
- // Remove possible arguments after search expression and ' [!=]= 0' suffix.
+ // Remove possible arguments after search expression and ' [!=]= .+' suffix.
Diagnostic << FixItHint::CreateReplacement(
CharSourceRange::getTokenRange(
Lexer::getLocForEndOfToken(SearchExpr->getEndLoc(), 0,
@@ -164,16 +204,16 @@ void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
ComparisonExpr->getEndLoc()),
")");
- // Remove possible '0 [!=]= ' prefix.
+ // Remove possible '.+ [!=]= ' prefix.
Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
- // Replace method name by 'starts_with'.
+ // Replace method name by '(starts|ends)_with'.
// Remove possible arguments before search expression.
Diagnostic << FixItHint::CreateReplacement(
CharSourceRange::getCharRange(FindExpr->getExprLoc(),
SearchExpr->getBeginLoc()),
- (StartsWithFunction->getName() + "(").str());
+ (ReplacementFunction->getName() + "(").str());
// Add possible negation '!'.
if (Neg) {
diff --git a/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/string b/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/string
index 0c160bc182b6eb..a0e8ebbb267cd0 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/string
+++ b/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/string
@@ -64,6 +64,10 @@ struct basic_string {
constexpr bool starts_with(C ch) const noexcept;
constexpr bool starts_with(const C* s) const;
+ constexpr bool ends_with(std::basic_string_view<C, T> sv) const noexcept;
+ constexpr bool ends_with(C ch) const noexcept;
+ constexpr bool ends_with(const C* s) const;
+
_Type& operator[](size_type);
const _Type& operator[](size_type) const;
@@ -108,6 +112,10 @@ struct basic_string_view {
constexpr bool starts_with(C ch) const noexcept;
constexpr bool starts_with(const C* s) const;
+ constexpr bool ends_with(basic_string_view sv) const noexcept;
+ constexpr bool ends_with(C ch) const noexcept;
+ constexpr bool ends_with(const C* s) const;
+
constexpr int compare(basic_string_view sv) const noexcept;
static constexpr size_t npos = -1;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-starts-ends-with.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-starts-ends-with.cpp
index c5b2c86befd1fe..798af260a3b66c 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-starts-ends-with.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-starts-ends-with.cpp
@@ -208,6 +208,62 @@ void test(std::string s, std::string_view sv, sub_string ss, sub_sub_string sss,
// CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use starts_with
// CHECK-FIXES: !s.starts_with(sv);
+ s.compare(s.size() - 6, 6, "suffix") == 0;
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with("suffix");
+
+ s.compare(s.size() - 6, strlen("abcdef"), "suffix") == 0;
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with("suffix");
+
+ std::string suffix = "suffix";
+ s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind("suffix") == s.size() - 6;
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with("suffix");
+
+ s.rfind("suffix") == s.size() - strlen("suffix");
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with("suffix");
+
+ s.rfind(suffix) == s.size() - suffix.size();
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind(suffix, std::string::npos) == s.size() - suffix.size();
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind(suffix) == (s.size() - suffix.size());
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind(suffix, s.npos) == (s.size() - suffix.size());
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind(suffix, s.npos) == (((s.size()) - (suffix.size())));
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ s.rfind(suffix) != s.size() - suffix.size();
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: !s.ends_with(suffix);
+
+ (s.size() - suffix.size()) == s.rfind(suffix);
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: s.ends_with(suffix);
+
+ struct S {
+ std::string s;
+ } t;
+ t.s.rfind(suffix) == (t.s.size() - suffix.size());
+ // CHECK-MESSAGES: :[[@LINE-1]]:{{[0-9]+}}: warning: use ends_with
+ // CHECK-FIXES: t.s.ends_with(suffix);
+
// Expressions that don't trigger the check are here.
#define EQ(x, y) ((x) == (y))
EQ(s.find("a"), 0);
@@ -219,4 +275,5 @@ void test(std::string s, std::string_view sv, sub_string ss, sub_sub_string sss,
STARTS_WITH_COMPARE(s, s) == 0;
s.compare(0, 1, "ab") == 0;
+ s.rfind(suffix, 1) == s.size() - suffix.size();
}
More information about the cfe-commits
mailing list