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 | |
19 | namespace clang::tidy::performance { |
20 | namespace { |
21 | |
22 | using namespace ::clang::ast_matchers; |
23 | using llvm::StringRef; |
24 | using utils::decl_ref_expr::allDeclRefExprs; |
25 | using utils::decl_ref_expr::isOnlyUsedAsConst; |
26 | |
27 | static constexpr StringRef ObjectArgId = "objectArg"; |
28 | static constexpr StringRef InitFunctionCallId = "initFunctionCall"; |
29 | static constexpr StringRef MethodDeclId = "methodDecl"; |
30 | static constexpr StringRef FunctionDeclId = "functionDecl"; |
31 | static constexpr StringRef OldVarDeclId = "oldVarDecl"; |
32 | |
33 | void 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 | |
43 | std::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 | |
54 | void 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 | |
77 | AST_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 | |
106 | AST_MATCHER(CXXMethodDecl, isStatic) { return Node.isStatic(); } |
107 | |
108 | AST_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 | |
121 | AST_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. |
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(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 | |
183 | bool isVariableUnused(const VarDecl &Var, const Stmt &BlockStmt, |
184 | ASTContext &Context) { |
185 | return allDeclRefExprs(VarDecl: Var, Stmt: BlockStmt, Context).empty(); |
186 | } |
187 | |
188 | const 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 | |
197 | bool 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 | |
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>(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 | |
229 | UnnecessaryCopyInitialization::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 | |
237 | void 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 | |
278 | void 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 | |
327 | void 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 | |
339 | void 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 | |
348 | void 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 | |
361 | void 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 | |
372 | void 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 | |
382 | void 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 |
Definitions
- ObjectArgId
- InitFunctionCallId
- MethodDeclId
- FunctionDeclId
- OldVarDeclId
- recordFixes
- firstLocAfterNewLine
- recordRemoval
- isRefReturningMethodCallWithConstOverloads
- isStatic
- isConstRefReturningFunctionCall
- initializerReturnsReferenceToConst
- isInitializingVariableImmutable
- isVariableUnused
- getSubstitutedType
- differentReplacedTemplateParams
- constructorArgumentType
- UnnecessaryCopyInitialization
- registerMatchers
- check
- handleCopyFromMethodReturn
- handleCopyFromLocalVar
- diagnoseCopyFromMethodReturn
- diagnoseCopyFromLocalVar
- maybeIssueFixes
Learn to use CMake with our Intro Training
Find out more