xref: /llvm-project/clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp (revision a5364444bd0dbbd60e83ee6cf11adabd72f4df54)
1 //===--- UnnecessaryCopyInitialization.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 "UnnecessaryCopyInitialization.h"
10 #include "../utils/DeclRefExprUtils.h"
11 #include "../utils/FixItHintUtils.h"
12 #include "../utils/LexerUtils.h"
13 #include "../utils/Matchers.h"
14 #include "../utils/OptionsUtils.h"
15 #include "clang/AST/Decl.h"
16 #include "clang/Basic/Diagnostic.h"
17 #include <optional>
18 #include <utility>
19 
20 namespace clang::tidy::performance {
21 namespace {
22 
23 using namespace ::clang::ast_matchers;
24 using llvm::StringRef;
25 using utils::decl_ref_expr::allDeclRefExprs;
26 using utils::decl_ref_expr::isOnlyUsedAsConst;
27 
28 static constexpr StringRef ObjectArgId = "objectArg";
29 static constexpr StringRef InitFunctionCallId = "initFunctionCall";
30 static constexpr StringRef MethodDeclId = "methodDecl";
31 static constexpr StringRef FunctionDeclId = "functionDecl";
32 static constexpr StringRef OldVarDeclId = "oldVarDecl";
33 
34 void recordFixes(const VarDecl &Var, ASTContext &Context,
35                  DiagnosticBuilder &Diagnostic) {
36   Diagnostic << utils::fixit::changeVarDeclToReference(Var, Context);
37   if (!Var.getType().isLocalConstQualified()) {
38     if (std::optional<FixItHint> Fix = utils::fixit::addQualifierToVarDecl(
39             Var, Context, Qualifiers::Const))
40       Diagnostic << *Fix;
41   }
42 }
43 
44 std::optional<SourceLocation> firstLocAfterNewLine(SourceLocation Loc,
45                                                    SourceManager &SM) {
46   bool Invalid = false;
47   const char *TextAfter = SM.getCharacterData(Loc, &Invalid);
48   if (Invalid) {
49     return std::nullopt;
50   }
51   size_t Offset = std::strcspn(TextAfter, "\n");
52   return Loc.getLocWithOffset(TextAfter[Offset] == '\0' ? Offset : Offset + 1);
53 }
54 
55 void recordRemoval(const DeclStmt &Stmt, ASTContext &Context,
56                    DiagnosticBuilder &Diagnostic) {
57   auto &SM = Context.getSourceManager();
58   // Attempt to remove trailing comments as well.
59   auto Tok = utils::lexer::findNextTokenSkippingComments(Stmt.getEndLoc(), SM,
60                                                          Context.getLangOpts());
61   std::optional<SourceLocation> PastNewLine =
62       firstLocAfterNewLine(Stmt.getEndLoc(), SM);
63   if (Tok && PastNewLine) {
64     auto BeforeFirstTokenAfterComment = Tok->getLocation().getLocWithOffset(-1);
65     // Remove until the end of the line or the end of a trailing comment which
66     // ever comes first.
67     auto End =
68         SM.isBeforeInTranslationUnit(*PastNewLine, BeforeFirstTokenAfterComment)
69             ? *PastNewLine
70             : BeforeFirstTokenAfterComment;
71     Diagnostic << FixItHint::CreateRemoval(
72         SourceRange(Stmt.getBeginLoc(), End));
73   } else {
74     Diagnostic << FixItHint::CreateRemoval(Stmt.getSourceRange());
75   }
76 }
77 
78 AST_MATCHER_FUNCTION_P(StatementMatcher,
79                        isRefReturningMethodCallWithConstOverloads,
80                        std::vector<StringRef>, ExcludedContainerTypes) {
81   // Match method call expressions where the `this` argument is only used as
82   // const, this will be checked in `check()` part. This returned reference is
83   // highly likely to outlive the local const reference of the variable being
84   // declared. The assumption is that the reference being returned either points
85   // to a global static variable or to a member of the called object.
86   const auto MethodDecl =
87       cxxMethodDecl(returns(hasCanonicalType(referenceType())))
88           .bind(MethodDeclId);
89   const auto ReceiverExpr =
90       ignoringParenImpCasts(declRefExpr(to(varDecl().bind(ObjectArgId))));
91   const auto OnExpr = anyOf(
92       // Direct reference to `*this`: `a.f()` or `a->f()`.
93       ReceiverExpr,
94       // Access through dereference, typically used for `operator[]`: `(*a)[3]`.
95       unaryOperator(hasOperatorName("*"), hasUnaryOperand(ReceiverExpr)));
96   const auto ReceiverType =
97       hasCanonicalType(recordType(hasDeclaration(namedDecl(
98           unless(matchers::matchesAnyListedName(ExcludedContainerTypes))))));
99 
100   return expr(
101       anyOf(cxxMemberCallExpr(callee(MethodDecl), on(OnExpr),
102                               thisPointerType(ReceiverType)),
103             cxxOperatorCallExpr(callee(MethodDecl), hasArgument(0, OnExpr),
104                                 hasArgument(0, hasType(ReceiverType)))));
105 }
106 
107 AST_MATCHER(CXXMethodDecl, isStatic) { return Node.isStatic(); }
108 
109 AST_MATCHER_FUNCTION(StatementMatcher, isConstRefReturningFunctionCall) {
110   // Only allow initialization of a const reference from a free function or
111   // static member function if it has no arguments. Otherwise it could return
112   // an alias to one of its arguments and the arguments need to be checked
113   // for const use as well.
114   return callExpr(argumentCountIs(0),
115                   callee(functionDecl(returns(hasCanonicalType(matchers::isReferenceToConst())),
116                                       unless(cxxMethodDecl(unless(isStatic()))))
117                          .bind(FunctionDeclId)))
118          .bind(InitFunctionCallId);
119 }
120 
121 AST_MATCHER_FUNCTION_P(StatementMatcher, initializerReturnsReferenceToConst,
122                        std::vector<StringRef>, ExcludedContainerTypes) {
123   auto OldVarDeclRef =
124       declRefExpr(to(varDecl(hasLocalStorage()).bind(OldVarDeclId)));
125   return expr(
126       anyOf(isConstRefReturningFunctionCall(),
127             isRefReturningMethodCallWithConstOverloads(ExcludedContainerTypes),
128             ignoringImpCasts(OldVarDeclRef),
129             ignoringImpCasts(unaryOperator(hasOperatorName("&"),
130                                            hasUnaryOperand(OldVarDeclRef)))));
131 }
132 
133 // This checks that the variable itself is only used as const, and also makes
134 // sure that it does not reference another variable that could be modified in
135 // the BlockStmt. It does this by checking the following:
136 // 1. If the variable is neither a reference nor a pointer then the
137 // isOnlyUsedAsConst() check is sufficient.
138 // 2. If the (reference or pointer) variable is not initialized in a DeclStmt in
139 // the BlockStmt. In this case its pointee is likely not modified (unless it
140 // is passed as an alias into the method as well).
141 // 3. If the reference is initialized from a reference to const. This is
142 // the same set of criteria we apply when identifying the unnecessary copied
143 // variable in this check to begin with. In this case we check whether the
144 // object arg or variable that is referenced is immutable as well.
145 static bool isInitializingVariableImmutable(
146     const VarDecl &InitializingVar, const Stmt &BlockStmt, ASTContext &Context,
147     const std::vector<StringRef> &ExcludedContainerTypes) {
148   QualType T = InitializingVar.getType().getCanonicalType();
149   if (!isOnlyUsedAsConst(InitializingVar, BlockStmt, Context,
150                          T->isPointerType() ? 1 : 0))
151     return false;
152 
153   // The variable is a value type and we know it is only used as const. Safe
154   // to reference it and avoid the copy.
155   if (!isa<ReferenceType, PointerType>(T))
156     return true;
157 
158   // The reference or pointer is not declared and hence not initialized anywhere
159   // in the function. We assume its pointee is not modified then.
160   if (!InitializingVar.isLocalVarDecl() || !InitializingVar.hasInit()) {
161     return true;
162   }
163 
164   auto Matches =
165       match(initializerReturnsReferenceToConst(ExcludedContainerTypes),
166             *InitializingVar.getInit(), Context);
167   // The reference is initialized from a free function without arguments
168   // returning a const reference. This is a global immutable object.
169   if (selectFirst<CallExpr>(InitFunctionCallId, Matches) != nullptr)
170     return true;
171   // Check that the object argument is immutable as well.
172   if (const auto *OrigVar = selectFirst<VarDecl>(ObjectArgId, Matches))
173     return isInitializingVariableImmutable(*OrigVar, BlockStmt, Context,
174                                            ExcludedContainerTypes);
175   // Check that the old variable we reference is immutable as well.
176   if (const auto *OrigVar = selectFirst<VarDecl>(OldVarDeclId, Matches))
177     return isInitializingVariableImmutable(*OrigVar, BlockStmt, Context,
178                                            ExcludedContainerTypes);
179 
180   return false;
181 }
182 
183 bool isVariableUnused(const VarDecl &Var, const Stmt &BlockStmt,
184                       ASTContext &Context) {
185   return allDeclRefExprs(Var, BlockStmt, Context).empty();
186 }
187 
188 const SubstTemplateTypeParmType *getSubstitutedType(const QualType &Type,
189                                                     ASTContext &Context) {
190   auto Matches = match(
191       qualType(anyOf(substTemplateTypeParmType().bind("subst"),
192                      hasDescendant(substTemplateTypeParmType().bind("subst")))),
193       Type, Context);
194   return selectFirst<SubstTemplateTypeParmType>("subst", Matches);
195 }
196 
197 bool differentReplacedTemplateParams(const QualType &VarType,
198                                      const QualType &InitializerType,
199                                      ASTContext &Context) {
200   if (const SubstTemplateTypeParmType *VarTmplType =
201           getSubstitutedType(VarType, Context)) {
202     if (const SubstTemplateTypeParmType *InitializerTmplType =
203             getSubstitutedType(InitializerType, Context)) {
204       const TemplateTypeParmDecl *VarTTP = VarTmplType->getReplacedParameter();
205       const TemplateTypeParmDecl *InitTTP =
206           InitializerTmplType->getReplacedParameter();
207       return (VarTTP->getDepth() != InitTTP->getDepth() ||
208               VarTTP->getIndex() != InitTTP->getIndex() ||
209               VarTTP->isParameterPack() != InitTTP->isParameterPack());
210     }
211   }
212   return false;
213 }
214 
215 QualType constructorArgumentType(const VarDecl *OldVar,
216                                  const BoundNodes &Nodes) {
217   if (OldVar) {
218     return OldVar->getType();
219   }
220   if (const auto *FuncDecl = Nodes.getNodeAs<FunctionDecl>(FunctionDeclId)) {
221     return FuncDecl->getReturnType();
222   }
223   const auto *MethodDecl = Nodes.getNodeAs<CXXMethodDecl>(MethodDeclId);
224   return MethodDecl->getReturnType();
225 }
226 
227 } // namespace
228 
229 UnnecessaryCopyInitialization::UnnecessaryCopyInitialization(
230     StringRef Name, ClangTidyContext *Context)
231     : ClangTidyCheck(Name, Context),
232       AllowedTypes(
233           utils::options::parseStringList(Options.get("AllowedTypes", ""))),
234       ExcludedContainerTypes(utils::options::parseStringList(
235           Options.get("ExcludedContainerTypes", ""))) {}
236 
237 void UnnecessaryCopyInitialization::registerMatchers(MatchFinder *Finder) {
238   auto LocalVarCopiedFrom = [this](const ast_matchers::internal::Matcher<Expr> &CopyCtorArg) {
239     return compoundStmt(
240                forEachDescendant(
241                    declStmt(
242                        unless(has(decompositionDecl())),
243                        has(varDecl(hasLocalStorage(),
244                                    hasType(qualType(
245                                        hasCanonicalType(allOf(
246                                            matchers::isExpensiveToCopy(),
247                                            unless(hasDeclaration(namedDecl(
248                                                hasName("::std::function")))))),
249                                        unless(hasDeclaration(namedDecl(
250                                            matchers::matchesAnyListedName(
251                                                AllowedTypes)))))),
252                                    unless(isImplicit()),
253                                    hasInitializer(traverse(
254                                        TK_AsIs,
255                                        cxxConstructExpr(
256                                            hasDeclaration(cxxConstructorDecl(
257                                                isCopyConstructor())),
258                                            hasArgument(0, CopyCtorArg))
259                                            .bind("ctorCall"))))
260                                .bind("newVarDecl")))
261                        .bind("declStmt")))
262         .bind("blockStmt");
263   };
264 
265   Finder->addMatcher(
266       LocalVarCopiedFrom(anyOf(
267           isConstRefReturningFunctionCall(),
268           isRefReturningMethodCallWithConstOverloads(ExcludedContainerTypes))),
269       this);
270 
271   Finder->addMatcher(LocalVarCopiedFrom(declRefExpr(
272                          to(varDecl(hasLocalStorage()).bind(OldVarDeclId)))),
273                      this);
274 }
275 
276 void UnnecessaryCopyInitialization::check(
277     const MatchFinder::MatchResult &Result) {
278   const auto &NewVar = *Result.Nodes.getNodeAs<VarDecl>("newVarDecl");
279   const auto &BlockStmt = *Result.Nodes.getNodeAs<Stmt>("blockStmt");
280   const auto &VarDeclStmt = *Result.Nodes.getNodeAs<DeclStmt>("declStmt");
281   // Do not propose fixes if the DeclStmt has multiple VarDecls or in
282   // macros since we cannot place them correctly.
283   const bool IssueFix =
284       VarDeclStmt.isSingleDecl() && !NewVar.getLocation().isMacroID();
285   const bool IsVarUnused = isVariableUnused(NewVar, BlockStmt, *Result.Context);
286   const bool IsVarOnlyUsedAsConst =
287       isOnlyUsedAsConst(NewVar, BlockStmt, *Result.Context,
288                         // `NewVar` is always of non-pointer type.
289                         0);
290   const CheckContext Context{
291       NewVar,   BlockStmt,   VarDeclStmt,         *Result.Context,
292       IssueFix, IsVarUnused, IsVarOnlyUsedAsConst};
293   const auto *OldVar = Result.Nodes.getNodeAs<VarDecl>(OldVarDeclId);
294   const auto *ObjectArg = Result.Nodes.getNodeAs<VarDecl>(ObjectArgId);
295   const auto *CtorCall = Result.Nodes.getNodeAs<CXXConstructExpr>("ctorCall");
296 
297   TraversalKindScope RAII(*Result.Context, TK_AsIs);
298 
299   // A constructor that looks like T(const T& t, bool arg = false) counts as a
300   // copy only when it is called with default arguments for the arguments after
301   // the first.
302   for (unsigned int I = 1; I < CtorCall->getNumArgs(); ++I)
303     if (!CtorCall->getArg(I)->isDefaultArgument())
304       return;
305 
306   // Don't apply the check if the variable and its initializer have different
307   // replaced template parameter types. In this case the check triggers for a
308   // template instantiation where the substituted types are the same, but
309   // instantiations where the types differ and rely on implicit conversion would
310   // no longer compile if we switched to a reference.
311   if (differentReplacedTemplateParams(
312           Context.Var.getType(), constructorArgumentType(OldVar, Result.Nodes),
313           *Result.Context))
314     return;
315 
316   if (OldVar == nullptr) {
317     // `auto NewVar = functionCall();`
318     handleCopyFromMethodReturn(Context, ObjectArg);
319   } else {
320     // `auto NewVar = OldVar;`
321     handleCopyFromLocalVar(Context, *OldVar);
322   }
323 }
324 
325 void UnnecessaryCopyInitialization::handleCopyFromMethodReturn(
326     const CheckContext &Ctx, const VarDecl *ObjectArg) {
327   bool IsConstQualified = Ctx.Var.getType().isConstQualified();
328   if (!IsConstQualified && !Ctx.IsVarOnlyUsedAsConst)
329     return;
330   if (ObjectArg != nullptr &&
331       !isInitializingVariableImmutable(*ObjectArg, Ctx.BlockStmt, Ctx.ASTCtx,
332                                        ExcludedContainerTypes))
333     return;
334   diagnoseCopyFromMethodReturn(Ctx);
335 }
336 
337 void UnnecessaryCopyInitialization::handleCopyFromLocalVar(
338     const CheckContext &Ctx, const VarDecl &OldVar) {
339   if (!Ctx.IsVarOnlyUsedAsConst ||
340       !isInitializingVariableImmutable(OldVar, Ctx.BlockStmt, Ctx.ASTCtx,
341                                        ExcludedContainerTypes))
342     return;
343   diagnoseCopyFromLocalVar(Ctx, OldVar);
344 }
345 
346 void UnnecessaryCopyInitialization::diagnoseCopyFromMethodReturn(
347     const CheckContext &Ctx) {
348   auto Diagnostic =
349       diag(Ctx.Var.getLocation(),
350            "the %select{|const qualified }0variable %1 is "
351            "copy-constructed "
352            "from a const reference%select{%select{ but is only used as const "
353            "reference|}0| but is never used}2; consider "
354            "%select{making it a const reference|removing the statement}2")
355       << Ctx.Var.getType().isConstQualified() << &Ctx.Var << Ctx.IsVarUnused;
356   maybeIssueFixes(Ctx, Diagnostic);
357 }
358 
359 void UnnecessaryCopyInitialization::diagnoseCopyFromLocalVar(
360     const CheckContext &Ctx, const VarDecl &OldVar) {
361   auto Diagnostic =
362       diag(Ctx.Var.getLocation(),
363            "local copy %1 of the variable %0 is never modified%select{"
364            "| and never used}2; consider %select{avoiding the copy|removing "
365            "the statement}2")
366       << &OldVar << &Ctx.Var << Ctx.IsVarUnused;
367   maybeIssueFixes(Ctx, Diagnostic);
368 }
369 
370 void UnnecessaryCopyInitialization::maybeIssueFixes(
371     const CheckContext &Ctx, DiagnosticBuilder &Diagnostic) {
372   if (Ctx.IssueFix) {
373     if (Ctx.IsVarUnused)
374       recordRemoval(Ctx.VarDeclStmt, Ctx.ASTCtx, Diagnostic);
375     else
376       recordFixes(Ctx.Var, Ctx.ASTCtx, Diagnostic);
377   }
378 }
379 
380 void UnnecessaryCopyInitialization::storeOptions(
381     ClangTidyOptions::OptionMap &Opts) {
382   Options.store(Opts, "AllowedTypes",
383                 utils::options::serializeStringList(AllowedTypes));
384   Options.store(Opts, "ExcludedContainerTypes",
385                 utils::options::serializeStringList(ExcludedContainerTypes));
386 }
387 
388 } // namespace clang::tidy::performance
389