1 | //===--- StringFindStartswithCheck.cc - 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 "StringFindStartswithCheck.h" |
10 | |
11 | #include "../utils/OptionsUtils.h" |
12 | #include "clang/AST/ASTContext.h" |
13 | #include "clang/ASTMatchers/ASTMatchers.h" |
14 | #include "clang/Frontend/CompilerInstance.h" |
15 | #include "clang/Lex/Lexer.h" |
16 | #include "clang/Lex/Preprocessor.h" |
17 | |
18 | using namespace clang::ast_matchers; |
19 | |
20 | namespace clang::tidy::abseil { |
21 | |
22 | const auto DefaultStringLikeClasses = |
23 | "::std::basic_string;::std::basic_string_view" ; |
24 | |
25 | StringFindStartswithCheck::StringFindStartswithCheck(StringRef Name, |
26 | ClangTidyContext *Context) |
27 | : ClangTidyCheck(Name, Context), |
28 | StringLikeClasses(utils::options::parseStringList( |
29 | Option: Options.get(LocalName: "StringLikeClasses" , Default: DefaultStringLikeClasses))), |
30 | IncludeInserter(Options.getLocalOrGlobal(LocalName: "IncludeStyle" , |
31 | Default: utils::IncludeSorter::IS_LLVM), |
32 | areDiagsSelfContained()), |
33 | AbseilStringsMatchHeader( |
34 | Options.get(LocalName: "AbseilStringsMatchHeader" , Default: "absl/strings/match.h" )) {} |
35 | |
36 | void StringFindStartswithCheck::registerMatchers(MatchFinder *Finder) { |
37 | auto ZeroLiteral = integerLiteral(equals(Value: 0)); |
38 | auto StringClassMatcher = cxxRecordDecl(hasAnyName(StringLikeClasses)); |
39 | auto StringType = hasUnqualifiedDesugaredType( |
40 | InnerMatcher: recordType(hasDeclaration(InnerMatcher: StringClassMatcher))); |
41 | |
42 | auto StringFind = cxxMemberCallExpr( |
43 | // .find()-call on a string... |
44 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "find" )).bind(ID: "findfun" )), |
45 | on(InnerMatcher: hasType(InnerMatcher: StringType)), |
46 | // ... with some search expression ... |
47 | hasArgument(N: 0, InnerMatcher: expr().bind(ID: "needle" )), |
48 | // ... and either "0" as second argument or the default argument (also 0). |
49 | anyOf(hasArgument(N: 1, InnerMatcher: ZeroLiteral), hasArgument(N: 1, InnerMatcher: cxxDefaultArgExpr()))); |
50 | |
51 | Finder->addMatcher( |
52 | // Match [=!]= with a zero on one side and a string.find on the other. |
53 | NodeMatch: binaryOperator( |
54 | hasAnyOperatorName("==" , "!=" ), |
55 | hasOperands(Matcher1: ignoringParenImpCasts(InnerMatcher: ZeroLiteral), |
56 | Matcher2: ignoringParenImpCasts(InnerMatcher: StringFind.bind(ID: "findexpr" )))) |
57 | .bind(ID: "expr" ), |
58 | Action: this); |
59 | |
60 | auto StringRFind = cxxMemberCallExpr( |
61 | // .rfind()-call on a string... |
62 | callee(InnerMatcher: cxxMethodDecl(hasName(Name: "rfind" )).bind(ID: "findfun" )), |
63 | on(InnerMatcher: hasType(InnerMatcher: StringType)), |
64 | // ... with some search expression ... |
65 | hasArgument(N: 0, InnerMatcher: expr().bind(ID: "needle" )), |
66 | // ... and "0" as second argument. |
67 | hasArgument(N: 1, InnerMatcher: ZeroLiteral)); |
68 | |
69 | Finder->addMatcher( |
70 | // Match [=!]= with either a zero or npos on one side and a string.rfind |
71 | // on the other. |
72 | NodeMatch: binaryOperator( |
73 | hasAnyOperatorName("==" , "!=" ), |
74 | hasOperands(Matcher1: ignoringParenImpCasts(InnerMatcher: ZeroLiteral), |
75 | Matcher2: ignoringParenImpCasts(InnerMatcher: StringRFind.bind(ID: "findexpr" )))) |
76 | .bind(ID: "expr" ), |
77 | Action: this); |
78 | } |
79 | |
80 | void StringFindStartswithCheck::check(const MatchFinder::MatchResult &Result) { |
81 | const ASTContext &Context = *Result.Context; |
82 | const SourceManager &Source = Context.getSourceManager(); |
83 | |
84 | // Extract matching (sub)expressions |
85 | const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>(ID: "expr" ); |
86 | assert(ComparisonExpr != nullptr); |
87 | const auto *Needle = Result.Nodes.getNodeAs<Expr>(ID: "needle" ); |
88 | assert(Needle != nullptr); |
89 | const Expr *Haystack = Result.Nodes.getNodeAs<CXXMemberCallExpr>(ID: "findexpr" ) |
90 | ->getImplicitObjectArgument(); |
91 | assert(Haystack != nullptr); |
92 | const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>(ID: "findfun" ); |
93 | assert(FindFun != nullptr); |
94 | |
95 | bool Rev = FindFun->getName().contains("rfind" ); |
96 | |
97 | if (ComparisonExpr->getBeginLoc().isMacroID()) |
98 | return; |
99 | |
100 | // Get the source code blocks (as characters) for both the string object |
101 | // and the search expression |
102 | const StringRef NeedleExprCode = Lexer::getSourceText( |
103 | Range: CharSourceRange::getTokenRange(Needle->getSourceRange()), SM: Source, |
104 | LangOpts: Context.getLangOpts()); |
105 | const StringRef HaystackExprCode = Lexer::getSourceText( |
106 | Range: CharSourceRange::getTokenRange(Haystack->getSourceRange()), SM: Source, |
107 | LangOpts: Context.getLangOpts()); |
108 | |
109 | // Create the StartsWith string, negating if comparison was "!=". |
110 | bool Neg = ComparisonExpr->getOpcode() == BO_NE; |
111 | |
112 | // Create the warning message and a FixIt hint replacing the original expr. |
113 | auto Diagnostic = |
114 | diag(Loc: ComparisonExpr->getBeginLoc(), |
115 | Description: "use %select{absl::StartsWith|!absl::StartsWith}0 " |
116 | "instead of %select{find()|rfind()}1 %select{==|!=}0 0" ) |
117 | << Neg << Rev; |
118 | |
119 | Diagnostic << FixItHint::CreateReplacement( |
120 | ComparisonExpr->getSourceRange(), |
121 | ((Neg ? "!absl::StartsWith(" : "absl::StartsWith(" ) + HaystackExprCode + |
122 | ", " + NeedleExprCode + ")" ) |
123 | .str()); |
124 | |
125 | // Create a preprocessor #include FixIt hint (createIncludeInsertion checks |
126 | // whether this already exists). |
127 | Diagnostic << IncludeInserter.createIncludeInsertion( |
128 | FileID: Source.getFileID(SpellingLoc: ComparisonExpr->getBeginLoc()), |
129 | Header: AbseilStringsMatchHeader); |
130 | } |
131 | |
132 | void StringFindStartswithCheck::registerPPCallbacks( |
133 | const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { |
134 | IncludeInserter.registerPreprocessor(PP); |
135 | } |
136 | |
137 | void StringFindStartswithCheck::storeOptions( |
138 | ClangTidyOptions::OptionMap &Opts) { |
139 | Options.store(Options&: Opts, LocalName: "StringLikeClasses" , |
140 | Value: utils::options::serializeStringList(Strings: StringLikeClasses)); |
141 | Options.store(Options&: Opts, LocalName: "IncludeStyle" , Value: IncludeInserter.getStyle()); |
142 | Options.store(Options&: Opts, LocalName: "AbseilStringsMatchHeader" , Value: AbseilStringsMatchHeader); |
143 | } |
144 | |
145 | } // namespace clang::tidy::abseil |
146 | |