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(Value: 0)); |
26 | const auto HasStartsWithMethodWithName = [](const std::string &Name) { |
27 | return hasMethod( |
28 | InnerMatcher: cxxMethodDecl(hasName(Name), isConst(), parameterCountIs(N: 1)) |
29 | .bind(ID: "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(BaseSpecMatcher: hasType(InnerMatcher: hasCanonicalType(InnerMatcher: hasDeclaration( |
37 | InnerMatcher: 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(N: 1), hasArgument(N: 1, InnerMatcher: ZeroLiteral)), |
42 | // ... named find... |
43 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "find" )).bind(ID: "find_fun" )), |
44 | // ... on a class with a starts_with function. |
45 | on(InnerMatcher: hasType( |
46 | InnerMatcher: hasCanonicalType(InnerMatcher: hasDeclaration(InnerMatcher: ClassWithStartsWithFunction)))), |
47 | // Bind search expression. |
48 | hasArgument(N: 0, InnerMatcher: expr().bind(ID: "search_expr" ))); |
49 | |
50 | const auto RFindExpr = cxxMemberCallExpr( |
51 | // A method call with a second argument of zero... |
52 | hasArgument(N: 1, InnerMatcher: ZeroLiteral), |
53 | // ... named rfind... |
54 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "rfind" )).bind(ID: "find_fun" )), |
55 | // ... on a class with a starts_with function. |
56 | on(InnerMatcher: hasType( |
57 | InnerMatcher: hasCanonicalType(InnerMatcher: hasDeclaration(InnerMatcher: ClassWithStartsWithFunction)))), |
58 | // Bind search expression. |
59 | hasArgument(N: 0, InnerMatcher: expr().bind(ID: "search_expr" ))); |
60 | |
61 | // Match a string literal and an integer or strlen() call matching the length. |
62 | const auto HasStringLiteralAndLengthArgs = [](const auto StringArgIndex, |
63 | const auto LengthArgIndex) { |
64 | return allOf( |
65 | hasArgument(StringArgIndex, stringLiteral().bind(ID: "string_literal_arg" )), |
66 | hasArgument(LengthArgIndex, |
67 | anyOf(integerLiteral().bind(ID: "integer_literal_size_arg" ), |
68 | callExpr(callee(InnerMatcher: functionDecl(parameterCountIs(N: 1), |
69 | hasName(Name: "strlen" ))), |
70 | hasArgument(N: 0, InnerMatcher: stringLiteral().bind( |
71 | ID: "strlen_arg" )))))); |
72 | }; |
73 | |
74 | // Match a string variable and a call to length() or size(). |
75 | const auto HasStringVariableAndSizeCallArgs = [](const auto StringArgIndex, |
76 | const auto LengthArgIndex) { |
77 | return allOf( |
78 | hasArgument(StringArgIndex, declRefExpr(hasDeclaration( |
79 | InnerMatcher: decl().bind(ID: "string_var_decl" )))), |
80 | hasArgument(LengthArgIndex, |
81 | cxxMemberCallExpr( |
82 | callee(InnerMatcher: cxxMethodDecl(isConst(), parameterCountIs(N: 0), |
83 | hasAnyName("size" , "length" ))), |
84 | on(InnerMatcher: declRefExpr( |
85 | to(InnerMatcher: decl(equalsBoundNode(ID: "string_var_decl" )))))))); |
86 | }; |
87 | |
88 | // Match either one of the two cases above. |
89 | const auto HasStringAndLengthArgs = |
90 | [HasStringLiteralAndLengthArgs, HasStringVariableAndSizeCallArgs]( |
91 | const auto StringArgIndex, const auto LengthArgIndex) { |
92 | return anyOf( |
93 | HasStringLiteralAndLengthArgs(StringArgIndex, LengthArgIndex), |
94 | HasStringVariableAndSizeCallArgs(StringArgIndex, LengthArgIndex)); |
95 | }; |
96 | |
97 | const auto CompareExpr = cxxMemberCallExpr( |
98 | // A method call with three arguments... |
99 | argumentCountIs(N: 3), |
100 | // ... where the first argument is zero... |
101 | hasArgument(N: 0, InnerMatcher: ZeroLiteral), |
102 | // ... named compare... |
103 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "compare" )).bind(ID: "find_fun" )), |
104 | // ... on a class with a starts_with function... |
105 | on(InnerMatcher: hasType( |
106 | InnerMatcher: hasCanonicalType(InnerMatcher: hasDeclaration(InnerMatcher: ClassWithStartsWithFunction)))), |
107 | // ... where the third argument is some string and the second a length. |
108 | HasStringAndLengthArgs(2, 1), |
109 | // Bind search expression. |
110 | hasArgument(N: 2, InnerMatcher: expr().bind(ID: "search_expr" ))); |
111 | |
112 | Finder->addMatcher( |
113 | // Match [=!]= with a zero on one side and (r?)find|compare on the other. |
114 | NodeMatch: binaryOperator( |
115 | hasAnyOperatorName("==" , "!=" ), |
116 | hasOperands(Matcher1: cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr)) |
117 | .bind(ID: "find_expr" ), |
118 | Matcher2: ZeroLiteral)) |
119 | .bind(ID: "expr" ), |
120 | Action: this); |
121 | } |
122 | |
123 | void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) { |
124 | const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>(ID: "expr" ); |
125 | const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>(ID: "find_expr" ); |
126 | const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>(ID: "find_fun" ); |
127 | const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>(ID: "search_expr" ); |
128 | const auto *StartsWithFunction = |
129 | Result.Nodes.getNodeAs<CXXMethodDecl>(ID: "starts_with_fun" ); |
130 | |
131 | const auto *StringLiteralArg = |
132 | Result.Nodes.getNodeAs<StringLiteral>(ID: "string_literal_arg" ); |
133 | const auto *IntegerLiteralSizeArg = |
134 | Result.Nodes.getNodeAs<IntegerLiteral>(ID: "integer_literal_size_arg" ); |
135 | const auto *StrlenArg = Result.Nodes.getNodeAs<StringLiteral>(ID: "strlen_arg" ); |
136 | |
137 | // Filter out compare cases where the length does not match string literal. |
138 | if (StringLiteralArg && IntegerLiteralSizeArg && |
139 | StringLiteralArg->getLength() != |
140 | IntegerLiteralSizeArg->getValue().getZExtValue()) { |
141 | return; |
142 | } |
143 | |
144 | if (StringLiteralArg && StrlenArg && |
145 | StringLiteralArg->getLength() != StrlenArg->getLength()) { |
146 | return; |
147 | } |
148 | |
149 | if (ComparisonExpr->getBeginLoc().isMacroID()) { |
150 | return; |
151 | } |
152 | |
153 | const bool Neg = ComparisonExpr->getOpcode() == BO_NE; |
154 | |
155 | auto Diagnostic = |
156 | diag(Loc: FindExpr->getExprLoc(), Description: "use %0 instead of %1() %select{==|!=}2 0" ) |
157 | << StartsWithFunction->getName() << FindFun->getName() << Neg; |
158 | |
159 | // Remove possible arguments after search expression and ' [!=]= 0' suffix. |
160 | Diagnostic << FixItHint::CreateReplacement( |
161 | CharSourceRange::getTokenRange( |
162 | Lexer::getLocForEndOfToken(Loc: SearchExpr->getEndLoc(), Offset: 0, |
163 | SM: *Result.SourceManager, LangOpts: getLangOpts()), |
164 | ComparisonExpr->getEndLoc()), |
165 | ")" ); |
166 | |
167 | // Remove possible '0 [!=]= ' prefix. |
168 | Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange( |
169 | ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc())); |
170 | |
171 | // Replace method name by 'starts_with'. |
172 | // Remove possible arguments before search expression. |
173 | Diagnostic << FixItHint::CreateReplacement( |
174 | CharSourceRange::getCharRange(FindExpr->getExprLoc(), |
175 | SearchExpr->getBeginLoc()), |
176 | (StartsWithFunction->getName() + "(" ).str()); |
177 | |
178 | // Add possible negation '!'. |
179 | if (Neg) { |
180 | Diagnostic << FixItHint::CreateInsertion(InsertionLoc: FindExpr->getBeginLoc(), Code: "!" ); |
181 | } |
182 | } |
183 | |
184 | } // namespace clang::tidy::modernize |
185 | |