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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp