| 1 | //===--- MoveConstArgCheck.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 "MoveConstArgCheck.h" |
| 10 | |
| 11 | #include "clang/Lex/Lexer.h" |
| 12 | |
| 13 | using namespace clang::ast_matchers; |
| 14 | |
| 15 | namespace clang::tidy::performance { |
| 16 | |
| 17 | static void replaceCallWithArg(const CallExpr *Call, DiagnosticBuilder &Diag, |
| 18 | const SourceManager &SM, |
| 19 | const LangOptions &LangOpts) { |
| 20 | const Expr *Arg = Call->getArg(Arg: 0); |
| 21 | |
| 22 | CharSourceRange BeforeArgumentsRange = Lexer::makeFileCharRange( |
| 23 | Range: CharSourceRange::getCharRange(Call->getBeginLoc(), Arg->getBeginLoc()), |
| 24 | SM, LangOpts); |
| 25 | CharSourceRange AfterArgumentsRange = Lexer::makeFileCharRange( |
| 26 | Range: CharSourceRange::getCharRange(B: Call->getEndLoc(), |
| 27 | E: Call->getEndLoc().getLocWithOffset(Offset: 1)), |
| 28 | SM, LangOpts); |
| 29 | |
| 30 | if (BeforeArgumentsRange.isValid() && AfterArgumentsRange.isValid()) { |
| 31 | Diag << FixItHint::CreateRemoval(RemoveRange: BeforeArgumentsRange) |
| 32 | << FixItHint::CreateRemoval(RemoveRange: AfterArgumentsRange); |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | void MoveConstArgCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
| 37 | Options.store(Options&: Opts, LocalName: "CheckTriviallyCopyableMove" , Value: CheckTriviallyCopyableMove); |
| 38 | Options.store(Options&: Opts, LocalName: "CheckMoveToConstRef" , Value: CheckMoveToConstRef); |
| 39 | } |
| 40 | |
| 41 | void MoveConstArgCheck::registerMatchers(MatchFinder *Finder) { |
| 42 | auto MoveCallMatcher = |
| 43 | callExpr(callee(InnerMatcher: functionDecl(hasName(Name: "::std::move" ))), argumentCountIs(N: 1), |
| 44 | unless(isInTemplateInstantiation())) |
| 45 | .bind(ID: "call-move" ); |
| 46 | |
| 47 | // Match ternary expressions where either branch contains std::move |
| 48 | auto TernaryWithMoveMatcher = |
| 49 | conditionalOperator(hasDescendant(MoveCallMatcher)); |
| 50 | |
| 51 | Finder->addMatcher( |
| 52 | NodeMatch: expr(anyOf( |
| 53 | castExpr(hasSourceExpression(InnerMatcher: MoveCallMatcher)), |
| 54 | cxxConstructExpr(hasDeclaration(InnerMatcher: cxxConstructorDecl(anyOf( |
| 55 | isCopyConstructor(), isMoveConstructor()))), |
| 56 | hasArgument(N: 0, InnerMatcher: MoveCallMatcher)))), |
| 57 | Action: this); |
| 58 | |
| 59 | auto ConstTypeParmMatcher = |
| 60 | qualType(references(InnerMatcher: isConstQualified())).bind(ID: "invocation-parm-type" ); |
| 61 | auto RValueTypeParmMatcher = |
| 62 | qualType(rValueReferenceType()).bind(ID: "invocation-parm-type" ); |
| 63 | // Matches respective ParmVarDecl for a CallExpr or CXXConstructExpr. |
| 64 | auto ArgumentWithParamMatcher = forEachArgumentWithParam( |
| 65 | ArgMatcher: anyOf(MoveCallMatcher, TernaryWithMoveMatcher), |
| 66 | ParamMatcher: parmVarDecl( |
| 67 | anyOf(hasType(InnerMatcher: ConstTypeParmMatcher), hasType(InnerMatcher: RValueTypeParmMatcher))) |
| 68 | .bind(ID: "invocation-parm" )); |
| 69 | // Matches respective types of arguments for a CallExpr or CXXConstructExpr |
| 70 | // and it works on calls through function pointers as well. |
| 71 | auto ArgumentWithParamTypeMatcher = forEachArgumentWithParamType( |
| 72 | ArgMatcher: anyOf(MoveCallMatcher, TernaryWithMoveMatcher), |
| 73 | ParamMatcher: anyOf(ConstTypeParmMatcher, RValueTypeParmMatcher)); |
| 74 | |
| 75 | Finder->addMatcher( |
| 76 | NodeMatch: invocation(anyOf(ArgumentWithParamMatcher, ArgumentWithParamTypeMatcher)) |
| 77 | .bind(ID: "receiving-expr" ), |
| 78 | Action: this); |
| 79 | } |
| 80 | |
| 81 | static bool isRValueReferenceParam(const Expr *Invocation, |
| 82 | const QualType *InvocationParmType, |
| 83 | const Expr *Arg) { |
| 84 | if (Invocation && (*InvocationParmType)->isRValueReferenceType() && |
| 85 | Arg->isLValue()) { |
| 86 | if (!Invocation->getType()->isRecordType()) |
| 87 | return true; |
| 88 | if (const auto *ConstructCallExpr = |
| 89 | dyn_cast<CXXConstructExpr>(Val: Invocation)) { |
| 90 | if (const auto *ConstructorDecl = ConstructCallExpr->getConstructor()) { |
| 91 | if (!ConstructorDecl->isCopyOrMoveConstructor() && |
| 92 | !ConstructorDecl->isDefaultConstructor()) |
| 93 | return true; |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | return false; |
| 98 | } |
| 99 | |
| 100 | void MoveConstArgCheck::check(const MatchFinder::MatchResult &Result) { |
| 101 | const auto *CallMove = Result.Nodes.getNodeAs<CallExpr>(ID: "call-move" ); |
| 102 | const auto *ReceivingExpr = Result.Nodes.getNodeAs<Expr>(ID: "receiving-expr" ); |
| 103 | const auto *InvocationParm = |
| 104 | Result.Nodes.getNodeAs<ParmVarDecl>(ID: "invocation-parm" ); |
| 105 | const auto *InvocationParmType = |
| 106 | Result.Nodes.getNodeAs<QualType>(ID: "invocation-parm-type" ); |
| 107 | |
| 108 | // Skipping matchers which have been matched. |
| 109 | if (!ReceivingExpr && AlreadyCheckedMoves.contains(V: CallMove)) |
| 110 | return; |
| 111 | |
| 112 | if (ReceivingExpr) |
| 113 | AlreadyCheckedMoves.insert(V: CallMove); |
| 114 | |
| 115 | const Expr *Arg = CallMove->getArg(Arg: 0); |
| 116 | const QualType ArgType = Arg->getType().getCanonicalType(); |
| 117 | SourceManager &SM = Result.Context->getSourceManager(); |
| 118 | |
| 119 | CharSourceRange MoveRange = |
| 120 | CharSourceRange::getCharRange(CallMove->getSourceRange()); |
| 121 | CharSourceRange FileMoveRange = |
| 122 | Lexer::makeFileCharRange(Range: MoveRange, SM, LangOpts: getLangOpts()); |
| 123 | if (!FileMoveRange.isValid()) |
| 124 | return; |
| 125 | |
| 126 | bool IsConstArg = ArgType.isConstQualified(); |
| 127 | bool IsTriviallyCopyable = ArgType.isTriviallyCopyableType(Context: *Result.Context); |
| 128 | |
| 129 | if (IsConstArg || IsTriviallyCopyable) { |
| 130 | if (const CXXRecordDecl *R = ArgType->getAsCXXRecordDecl()) { |
| 131 | // According to [expr.prim.lambda]p3, "whether the closure type is |
| 132 | // trivially copyable" property can be changed by the implementation of |
| 133 | // the language, so we shouldn't rely on it when issuing diagnostics. |
| 134 | if (R->isLambda()) |
| 135 | return; |
| 136 | // Don't warn when the type is not copyable. |
| 137 | for (const auto *Ctor : R->ctors()) { |
| 138 | if (Ctor->isCopyConstructor() && Ctor->isDeleted()) |
| 139 | return; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | if (!IsConstArg && IsTriviallyCopyable && !CheckTriviallyCopyableMove) |
| 144 | return; |
| 145 | |
| 146 | bool IsVariable = isa<DeclRefExpr>(Val: Arg); |
| 147 | // std::move shouldn't be removed when an lvalue wrapped by std::move is |
| 148 | // passed to the function with an rvalue reference parameter. |
| 149 | bool IsRVRefParam = |
| 150 | isRValueReferenceParam(Invocation: ReceivingExpr, InvocationParmType, Arg); |
| 151 | const auto *Var = |
| 152 | IsVariable ? dyn_cast<DeclRefExpr>(Val: Arg)->getDecl() : nullptr; |
| 153 | |
| 154 | { |
| 155 | auto Diag = diag(Loc: FileMoveRange.getBegin(), |
| 156 | Description: "std::move of the %select{|const }0" |
| 157 | "%select{expression|variable %5}1 " |
| 158 | "%select{|of the trivially-copyable type %6 }2" |
| 159 | "has no effect%select{; remove std::move()|}3" |
| 160 | "%select{| or make the variable non-const}4" ) |
| 161 | << IsConstArg << IsVariable << IsTriviallyCopyable |
| 162 | << IsRVRefParam |
| 163 | << (IsConstArg && IsVariable && !IsTriviallyCopyable) << Var |
| 164 | << Arg->getType(); |
| 165 | if (!IsRVRefParam) |
| 166 | replaceCallWithArg(CallMove, Diag, SM, getLangOpts()); |
| 167 | } |
| 168 | if (IsRVRefParam) { |
| 169 | // Generate notes for an invocation with an rvalue reference parameter. |
| 170 | const auto *ReceivingCallExpr = dyn_cast<CallExpr>(Val: ReceivingExpr); |
| 171 | const auto *ReceivingConstructExpr = |
| 172 | dyn_cast<CXXConstructExpr>(Val: ReceivingExpr); |
| 173 | // Skipping the invocation which is a template instantiation. |
| 174 | if ((!ReceivingCallExpr || !ReceivingCallExpr->getDirectCallee() || |
| 175 | ReceivingCallExpr->getDirectCallee()->isTemplateInstantiation()) && |
| 176 | (!ReceivingConstructExpr || |
| 177 | !ReceivingConstructExpr->getConstructor() || |
| 178 | ReceivingConstructExpr->getConstructor()->isTemplateInstantiation())) |
| 179 | return; |
| 180 | |
| 181 | const NamedDecl *FunctionName = nullptr; |
| 182 | FunctionName = |
| 183 | ReceivingCallExpr |
| 184 | ? ReceivingCallExpr->getDirectCallee()->getUnderlyingDecl() |
| 185 | : ReceivingConstructExpr->getConstructor()->getUnderlyingDecl(); |
| 186 | |
| 187 | QualType NoRefType = (*InvocationParmType)->getPointeeType(); |
| 188 | PrintingPolicy PolicyWithSuppressedTag(getLangOpts()); |
| 189 | PolicyWithSuppressedTag.SuppressTagKeyword = true; |
| 190 | PolicyWithSuppressedTag.SuppressUnwrittenScope = true; |
| 191 | std::string ExpectParmTypeName = |
| 192 | NoRefType.getAsString(Policy: PolicyWithSuppressedTag); |
| 193 | if (!NoRefType->isPointerType()) { |
| 194 | NoRefType.addConst(); |
| 195 | ExpectParmTypeName = |
| 196 | NoRefType.getAsString(Policy: PolicyWithSuppressedTag) + " &" ; |
| 197 | } |
| 198 | |
| 199 | diag(InvocationParm->getLocation(), |
| 200 | "consider changing the %ordinal0 parameter of %1 from %2 to '%3'" , |
| 201 | DiagnosticIDs::Note) |
| 202 | << (InvocationParm->getFunctionScopeIndex() + 1) << FunctionName |
| 203 | << *InvocationParmType << ExpectParmTypeName; |
| 204 | } |
| 205 | } else if (ReceivingExpr && CheckMoveToConstRef) { |
| 206 | if ((*InvocationParmType)->isRValueReferenceType()) |
| 207 | return; |
| 208 | |
| 209 | { |
| 210 | auto Diag = diag(Loc: FileMoveRange.getBegin(), |
| 211 | Description: "passing result of std::move() as a const reference " |
| 212 | "argument; no move will actually happen" ); |
| 213 | |
| 214 | replaceCallWithArg(CallMove, Diag, SM, getLangOpts()); |
| 215 | } |
| 216 | |
| 217 | if (const CXXRecordDecl *RecordDecl = ArgType->getAsCXXRecordDecl(); |
| 218 | RecordDecl && RecordDecl->hasDefinition() && |
| 219 | !(RecordDecl->hasMoveConstructor() && |
| 220 | RecordDecl->hasMoveAssignment())) { |
| 221 | const bool MissingMoveAssignment = !RecordDecl->hasMoveAssignment(); |
| 222 | const bool MissingMoveConstructor = !RecordDecl->hasMoveConstructor(); |
| 223 | const bool MissingBoth = MissingMoveAssignment && MissingMoveConstructor; |
| 224 | |
| 225 | diag(RecordDecl->getLocation(), |
| 226 | "%0 is not move " |
| 227 | "%select{|assignable}1%select{|/}2%select{|constructible}3" , |
| 228 | DiagnosticIDs::Note) |
| 229 | << RecordDecl << MissingMoveAssignment << MissingBoth |
| 230 | << MissingMoveConstructor; |
| 231 | } |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | } // namespace clang::tidy::performance |
| 236 | |