1 | //===- RedundantStringCStrCheck.cpp - Check for redundant c_str calls -----===// |
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 | // This file implements a check for redundant calls of c_str() on strings. |
10 | // |
11 | //===----------------------------------------------------------------------===// |
12 | |
13 | #include "RedundantStringCStrCheck.h" |
14 | #include "../utils/FixItHintUtils.h" |
15 | #include "../utils/Matchers.h" |
16 | #include "../utils/OptionsUtils.h" |
17 | #include "clang/Lex/Lexer.h" |
18 | #include "clang/Tooling/FixIt.h" |
19 | |
20 | using namespace clang::ast_matchers; |
21 | |
22 | namespace clang::tidy::readability { |
23 | |
24 | namespace { |
25 | |
26 | AST_MATCHER(MaterializeTemporaryExpr, isBoundToLValue) { |
27 | return Node.isBoundToLvalueReference(); |
28 | } |
29 | |
30 | } // end namespace |
31 | |
32 | RedundantStringCStrCheck::RedundantStringCStrCheck(StringRef Name, |
33 | ClangTidyContext *Context) |
34 | : ClangTidyCheck(Name, Context), |
35 | StringParameterFunctions(utils::options::parseStringList( |
36 | Option: Options.get(LocalName: "StringParameterFunctions" , Default: "" ))) { |
37 | if (getLangOpts().CPlusPlus20) |
38 | StringParameterFunctions.emplace_back(args: "::std::format" ); |
39 | if (getLangOpts().CPlusPlus23) |
40 | StringParameterFunctions.emplace_back(args: "::std::print" ); |
41 | } |
42 | |
43 | void RedundantStringCStrCheck::registerMatchers( |
44 | ast_matchers::MatchFinder *Finder) { |
45 | // Match expressions of type 'string' or 'string*'. |
46 | const auto StringDecl = type(hasUnqualifiedDesugaredType(InnerMatcher: recordType( |
47 | hasDeclaration(InnerMatcher: cxxRecordDecl(hasName(Name: "::std::basic_string" )))))); |
48 | const auto StringExpr = |
49 | expr(anyOf(hasType(InnerMatcher: StringDecl), hasType(InnerMatcher: qualType(pointsTo(InnerMatcher: StringDecl))))); |
50 | |
51 | // Match string constructor. |
52 | const auto StringConstructorExpr = expr(anyOf( |
53 | cxxConstructExpr(argumentCountIs(N: 1), |
54 | hasDeclaration(InnerMatcher: cxxMethodDecl(hasName(Name: "basic_string" )))), |
55 | cxxConstructExpr( |
56 | argumentCountIs(N: 2), |
57 | hasDeclaration(InnerMatcher: cxxMethodDecl(hasName(Name: "basic_string" ))), |
58 | // If present, the second argument is the alloc object which must not |
59 | // be present explicitly. |
60 | hasArgument(N: 1, InnerMatcher: cxxDefaultArgExpr())))); |
61 | |
62 | // Match string constructor. |
63 | const auto StringViewConstructorExpr = cxxConstructExpr( |
64 | argumentCountIs(N: 1), |
65 | hasDeclaration(InnerMatcher: cxxMethodDecl(hasName(Name: "basic_string_view" )))); |
66 | |
67 | // Match a call to the string 'c_str()' method. |
68 | const auto StringCStrCallExpr = |
69 | cxxMemberCallExpr(on(InnerMatcher: StringExpr.bind(ID: "arg" )), |
70 | callee(InnerMatcher: memberExpr().bind(ID: "member" )), |
71 | callee(InnerMatcher: cxxMethodDecl(hasAnyName("c_str" , "data" )))) |
72 | .bind(ID: "call" ); |
73 | const auto HasRValueTempParent = |
74 | hasParent(materializeTemporaryExpr(unless(isBoundToLValue()))); |
75 | // Detect redundant 'c_str()' calls through a string constructor. |
76 | // If CxxConstructExpr is the part of some CallExpr we need to |
77 | // check that matched ParamDecl of the ancestor CallExpr is not rvalue. |
78 | Finder->addMatcher( |
79 | NodeMatch: traverse( |
80 | TK: TK_AsIs, |
81 | InnerMatcher: cxxConstructExpr( |
82 | anyOf(StringConstructorExpr, StringViewConstructorExpr), |
83 | hasArgument(N: 0, InnerMatcher: StringCStrCallExpr), |
84 | unless(anyOf(HasRValueTempParent, hasParent(cxxBindTemporaryExpr( |
85 | HasRValueTempParent)))))), |
86 | Action: this); |
87 | |
88 | // Detect: 's == str.c_str()' -> 's == str' |
89 | Finder->addMatcher( |
90 | NodeMatch: cxxOperatorCallExpr( |
91 | hasAnyOverloadedOperatorName("<" , ">" , ">=" , "<=" , "!=" , "==" , "+" ), |
92 | anyOf(allOf(hasArgument(N: 0, InnerMatcher: StringExpr), |
93 | hasArgument(N: 1, InnerMatcher: StringCStrCallExpr)), |
94 | allOf(hasArgument(N: 0, InnerMatcher: StringCStrCallExpr), |
95 | hasArgument(N: 1, InnerMatcher: StringExpr)))), |
96 | Action: this); |
97 | |
98 | // Detect: 'dst += str.c_str()' -> 'dst += str' |
99 | // Detect: 's = str.c_str()' -> 's = str' |
100 | Finder->addMatcher( |
101 | NodeMatch: cxxOperatorCallExpr(hasAnyOverloadedOperatorName("=" , "+=" ), |
102 | hasArgument(N: 0, InnerMatcher: StringExpr), |
103 | hasArgument(N: 1, InnerMatcher: StringCStrCallExpr)), |
104 | Action: this); |
105 | |
106 | // Detect: 'dst.append(str.c_str())' -> 'dst.append(str)' |
107 | Finder->addMatcher( |
108 | NodeMatch: cxxMemberCallExpr(on(InnerMatcher: StringExpr), callee(InnerMatcher: decl(cxxMethodDecl(hasAnyName( |
109 | "append" , "assign" , "compare" )))), |
110 | argumentCountIs(N: 1), hasArgument(N: 0, InnerMatcher: StringCStrCallExpr)), |
111 | Action: this); |
112 | |
113 | // Detect: 'dst.compare(p, n, str.c_str())' -> 'dst.compare(p, n, str)' |
114 | Finder->addMatcher( |
115 | NodeMatch: cxxMemberCallExpr(on(InnerMatcher: StringExpr), |
116 | callee(InnerMatcher: decl(cxxMethodDecl(hasName(Name: "compare" )))), |
117 | argumentCountIs(N: 3), hasArgument(N: 2, InnerMatcher: StringCStrCallExpr)), |
118 | Action: this); |
119 | |
120 | // Detect: 'dst.find(str.c_str())' -> 'dst.find(str)' |
121 | Finder->addMatcher( |
122 | NodeMatch: cxxMemberCallExpr(on(InnerMatcher: StringExpr), |
123 | callee(InnerMatcher: decl(cxxMethodDecl(hasAnyName( |
124 | "find" , "find_first_not_of" , "find_first_of" , |
125 | "find_last_not_of" , "find_last_of" , "rfind" )))), |
126 | anyOf(argumentCountIs(N: 1), argumentCountIs(N: 2)), |
127 | hasArgument(N: 0, InnerMatcher: StringCStrCallExpr)), |
128 | Action: this); |
129 | |
130 | // Detect: 'dst.insert(pos, str.c_str())' -> 'dst.insert(pos, str)' |
131 | Finder->addMatcher( |
132 | NodeMatch: cxxMemberCallExpr(on(InnerMatcher: StringExpr), |
133 | callee(InnerMatcher: decl(cxxMethodDecl(hasName(Name: "insert" )))), |
134 | argumentCountIs(N: 2), hasArgument(N: 1, InnerMatcher: StringCStrCallExpr)), |
135 | Action: this); |
136 | |
137 | // Detect redundant 'c_str()' calls through a StringRef constructor. |
138 | Finder->addMatcher( |
139 | NodeMatch: traverse( |
140 | TK: TK_AsIs, |
141 | InnerMatcher: cxxConstructExpr( |
142 | // Implicit constructors of these classes are overloaded |
143 | // wrt. string types and they internally make a StringRef |
144 | // referring to the argument. Passing a string directly to |
145 | // them is preferred to passing a char pointer. |
146 | hasDeclaration(InnerMatcher: cxxMethodDecl(hasAnyName( |
147 | "::llvm::StringRef::StringRef" , "::llvm::Twine::Twine" ))), |
148 | argumentCountIs(N: 1), |
149 | // The only argument must have the form x.c_str() or p->c_str() |
150 | // where the method is string::c_str(). StringRef also has |
151 | // a constructor from string which is more efficient (avoids |
152 | // strlen), so we can construct StringRef from the string |
153 | // directly. |
154 | hasArgument(N: 0, InnerMatcher: StringCStrCallExpr))), |
155 | Action: this); |
156 | |
157 | if (!StringParameterFunctions.empty()) { |
158 | // Detect redundant 'c_str()' calls in parameters passed to std::format in |
159 | // C++20 onwards and std::print in C++23 onwards. |
160 | Finder->addMatcher( |
161 | NodeMatch: traverse(TK: TK_AsIs, |
162 | InnerMatcher: callExpr(callee(InnerMatcher: functionDecl(matchers::matchesAnyListedName( |
163 | NameList: StringParameterFunctions))), |
164 | forEachArgumentWithParam(ArgMatcher: StringCStrCallExpr, |
165 | ParamMatcher: parmVarDecl()))), |
166 | Action: this); |
167 | } |
168 | } |
169 | |
170 | void RedundantStringCStrCheck::check(const MatchFinder::MatchResult &Result) { |
171 | const auto *Call = Result.Nodes.getNodeAs<CallExpr>(ID: "call" ); |
172 | const auto *Arg = Result.Nodes.getNodeAs<Expr>(ID: "arg" ); |
173 | const auto *Member = Result.Nodes.getNodeAs<MemberExpr>(ID: "member" ); |
174 | bool Arrow = Member->isArrow(); |
175 | // Replace the "call" node with the "arg" node, prefixed with '*' |
176 | // if the call was using '->' rather than '.'. |
177 | std::string ArgText = |
178 | Arrow ? utils::fixit::formatDereference(ExprNode: *Arg, Context: *Result.Context) |
179 | : tooling::fixit::getText(Node: *Arg, Context: *Result.Context).str(); |
180 | if (ArgText.empty()) |
181 | return; |
182 | |
183 | diag(Loc: Call->getBeginLoc(), Description: "redundant call to %0" ) |
184 | << Member->getMemberDecl() |
185 | << FixItHint::CreateReplacement(Call->getSourceRange(), ArgText); |
186 | } |
187 | |
188 | } // namespace clang::tidy::readability |
189 | |