1 | //===--- RedundantStrcatCallsCheck.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 "RedundantStrcatCallsCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
12 | |
13 | using namespace clang::ast_matchers; |
14 | |
15 | namespace clang::tidy::abseil { |
16 | |
17 | // TODO: Features to add to the check: |
18 | // - Make it work if num_args > 26. |
19 | // - Remove empty literal string arguments. |
20 | // - Collapse consecutive literal string arguments into one (remove the ,). |
21 | // - Replace StrCat(a + b) -> StrCat(a, b) if a or b are strings. |
22 | // - Make it work in macros if the outer and inner StrCats are both in the |
23 | // argument. |
24 | |
25 | void RedundantStrcatCallsCheck::registerMatchers(MatchFinder* Finder) { |
26 | const auto CallToStrcat = |
27 | callExpr(callee(InnerMatcher: functionDecl(hasName(Name: "::absl::StrCat" )))); |
28 | const auto CallToStrappend = |
29 | callExpr(callee(InnerMatcher: functionDecl(hasName(Name: "::absl::StrAppend" )))); |
30 | // Do not match StrCat() calls that are descendants of other StrCat calls. |
31 | // Those are handled on the ancestor call. |
32 | const auto CallToEither = callExpr( |
33 | callee(InnerMatcher: functionDecl(hasAnyName("::absl::StrCat" , "::absl::StrAppend" )))); |
34 | Finder->addMatcher( |
35 | NodeMatch: callExpr(CallToStrcat, unless(hasAncestor(CallToEither))).bind(ID: "StrCat" ), |
36 | Action: this); |
37 | Finder->addMatcher(NodeMatch: CallToStrappend.bind(ID: "StrAppend" ), Action: this); |
38 | } |
39 | |
40 | namespace { |
41 | |
42 | struct StrCatCheckResult { |
43 | int NumCalls = 0; |
44 | std::vector<FixItHint> Hints; |
45 | }; |
46 | |
47 | void removeCallLeaveArgs(const CallExpr *Call, StrCatCheckResult *CheckResult) { |
48 | if (Call->getNumArgs() == 0) |
49 | return; |
50 | // Remove 'Foo(' |
51 | CheckResult->Hints.push_back( |
52 | FixItHint::CreateRemoval(CharSourceRange::getCharRange( |
53 | Call->getBeginLoc(), Call->getArg(Arg: 0)->getBeginLoc()))); |
54 | // Remove the ')' |
55 | CheckResult->Hints.push_back( |
56 | x: FixItHint::CreateRemoval(RemoveRange: CharSourceRange::getCharRange( |
57 | B: Call->getRParenLoc(), E: Call->getEndLoc().getLocWithOffset(Offset: 1)))); |
58 | } |
59 | |
60 | const clang::CallExpr *processArgument(const Expr *Arg, |
61 | const MatchFinder::MatchResult &Result, |
62 | StrCatCheckResult *CheckResult) { |
63 | const auto IsAlphanum = hasDeclaration(InnerMatcher: cxxMethodDecl(hasName(Name: "AlphaNum" ))); |
64 | static const auto* const Strcat = new auto(hasName(Name: "::absl::StrCat" )); |
65 | const auto IsStrcat = cxxBindTemporaryExpr( |
66 | has(callExpr(callee(InnerMatcher: functionDecl(*Strcat))).bind(ID: "StrCat" ))); |
67 | if (const auto *SubStrcatCall = selectFirst<const CallExpr>( |
68 | BoundTo: "StrCat" , |
69 | Results: match(Matcher: stmt(traverse(TK: TK_AsIs, |
70 | InnerMatcher: anyOf(cxxConstructExpr(IsAlphanum, |
71 | hasArgument(N: 0, InnerMatcher: IsStrcat)), |
72 | IsStrcat))), |
73 | Node: *Arg->IgnoreParenImpCasts(), Context&: *Result.Context))) { |
74 | removeCallLeaveArgs(Call: SubStrcatCall, CheckResult); |
75 | return SubStrcatCall; |
76 | } |
77 | return nullptr; |
78 | } |
79 | |
80 | StrCatCheckResult processCall(const CallExpr *RootCall, bool IsAppend, |
81 | const MatchFinder::MatchResult &Result) { |
82 | StrCatCheckResult CheckResult; |
83 | std::deque<const CallExpr*> CallsToProcess = {RootCall}; |
84 | |
85 | while (!CallsToProcess.empty()) { |
86 | ++CheckResult.NumCalls; |
87 | |
88 | const CallExpr* CallExpr = CallsToProcess.front(); |
89 | CallsToProcess.pop_front(); |
90 | |
91 | int StartArg = CallExpr == RootCall && IsAppend; |
92 | for (const auto *Arg : CallExpr->arguments()) { |
93 | if (StartArg-- > 0) |
94 | continue; |
95 | if (const clang::CallExpr *Sub = |
96 | processArgument(Arg, Result, &CheckResult)) { |
97 | CallsToProcess.push_back(x: Sub); |
98 | } |
99 | } |
100 | } |
101 | return CheckResult; |
102 | } |
103 | } // namespace |
104 | |
105 | void RedundantStrcatCallsCheck::check(const MatchFinder::MatchResult& Result) { |
106 | bool IsAppend = false; |
107 | |
108 | const CallExpr *RootCall = nullptr; |
109 | if ((RootCall = Result.Nodes.getNodeAs<CallExpr>(ID: "StrCat" ))) |
110 | IsAppend = false; |
111 | else if ((RootCall = Result.Nodes.getNodeAs<CallExpr>(ID: "StrAppend" ))) |
112 | IsAppend = true; |
113 | else |
114 | return; |
115 | |
116 | if (RootCall->getBeginLoc().isMacroID()) { |
117 | // Ignore calls within macros. |
118 | // In many cases the outer StrCat part of the macro and the inner StrCat is |
119 | // a macro argument. Removing the inner StrCat() converts one macro |
120 | // argument into many. |
121 | return; |
122 | } |
123 | |
124 | const StrCatCheckResult CheckResult = processCall(RootCall, IsAppend, Result); |
125 | if (CheckResult.NumCalls == 1) { |
126 | // Just one call, so nothing to fix. |
127 | return; |
128 | } |
129 | |
130 | diag(Loc: RootCall->getBeginLoc(), |
131 | Description: "multiple calls to 'absl::StrCat' can be flattened into a single call" ) |
132 | << CheckResult.Hints; |
133 | } |
134 | |
135 | } // namespace clang::tidy::abseil |
136 | |