| 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 | |