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