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