1 | //===--- VirtualClassDestructorCheck.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 "VirtualClassDestructorCheck.h" |
10 | #include "../utils/LexerUtils.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/Lex/Lexer.h" |
14 | #include <optional> |
15 | #include <string> |
16 | |
17 | using namespace clang::ast_matchers; |
18 | |
19 | namespace clang::tidy::cppcoreguidelines { |
20 | |
21 | namespace { |
22 | |
23 | AST_MATCHER(CXXRecordDecl, hasPublicVirtualOrProtectedNonVirtualDestructor) { |
24 | // We need to call Node.getDestructor() instead of matching a |
25 | // CXXDestructorDecl. Otherwise, tests will fail for class templates, since |
26 | // the primary template (not the specialization) always gets a non-virtual |
27 | // CXXDestructorDecl in the AST. https://bugs.llvm.org/show_bug.cgi?id=51912 |
28 | const CXXDestructorDecl *Destructor = Node.getDestructor(); |
29 | if (!Destructor) |
30 | return false; |
31 | |
32 | return (((Destructor->getAccess() == AccessSpecifier::AS_public) && |
33 | Destructor->isVirtual()) || |
34 | ((Destructor->getAccess() == AccessSpecifier::AS_protected) && |
35 | !Destructor->isVirtual())); |
36 | } |
37 | |
38 | } // namespace |
39 | |
40 | void VirtualClassDestructorCheck::registerMatchers(MatchFinder *Finder) { |
41 | ast_matchers::internal::Matcher<CXXRecordDecl> InheritsVirtualMethod = |
42 | hasAnyBase(BaseSpecMatcher: hasType(InnerMatcher: cxxRecordDecl(has(cxxMethodDecl(isVirtual()))))); |
43 | |
44 | Finder->addMatcher( |
45 | NodeMatch: cxxRecordDecl( |
46 | anyOf(has(cxxMethodDecl(isVirtual())), InheritsVirtualMethod), |
47 | unless(isFinal()), |
48 | unless(hasPublicVirtualOrProtectedNonVirtualDestructor())) |
49 | .bind(ID: "ProblematicClassOrStruct" ), |
50 | Action: this); |
51 | } |
52 | |
53 | static std::optional<CharSourceRange> |
54 | getVirtualKeywordRange(const CXXDestructorDecl &Destructor, |
55 | const SourceManager &SM, const LangOptions &LangOpts) { |
56 | if (Destructor.getLocation().isMacroID()) |
57 | return std::nullopt; |
58 | |
59 | SourceLocation VirtualBeginLoc = Destructor.getBeginLoc(); |
60 | SourceLocation VirtualBeginSpellingLoc = |
61 | SM.getSpellingLoc(Loc: Destructor.getBeginLoc()); |
62 | SourceLocation VirtualEndLoc = VirtualBeginSpellingLoc.getLocWithOffset( |
63 | Offset: Lexer::MeasureTokenLength(Loc: VirtualBeginSpellingLoc, SM, LangOpts)); |
64 | |
65 | /// Range ends with \c StartOfNextToken so that any whitespace after \c |
66 | /// virtual is included. |
67 | std::optional<Token> NextToken = |
68 | Lexer::findNextToken(Loc: VirtualEndLoc, SM, LangOpts); |
69 | if (!NextToken) |
70 | return std::nullopt; |
71 | SourceLocation StartOfNextToken = NextToken->getLocation(); |
72 | |
73 | return CharSourceRange::getCharRange(B: VirtualBeginLoc, E: StartOfNextToken); |
74 | } |
75 | |
76 | static const AccessSpecDecl * |
77 | getPublicASDecl(const CXXRecordDecl &StructOrClass) { |
78 | for (DeclContext::specific_decl_iterator<AccessSpecDecl> |
79 | AS{StructOrClass.decls_begin()}, |
80 | ASEnd{StructOrClass.decls_end()}; |
81 | AS != ASEnd; ++AS) { |
82 | AccessSpecDecl *ASDecl = *AS; |
83 | if (ASDecl->getAccess() == AccessSpecifier::AS_public) |
84 | return ASDecl; |
85 | } |
86 | |
87 | return nullptr; |
88 | } |
89 | |
90 | static FixItHint |
91 | generateUserDeclaredDestructor(const CXXRecordDecl &StructOrClass, |
92 | const SourceManager &SourceManager) { |
93 | std::string DestructorString; |
94 | SourceLocation Loc; |
95 | bool AppendLineBreak = false; |
96 | |
97 | const AccessSpecDecl *AccessSpecDecl = getPublicASDecl(StructOrClass); |
98 | |
99 | if (!AccessSpecDecl) { |
100 | if (StructOrClass.isClass()) { |
101 | Loc = StructOrClass.getEndLoc(); |
102 | DestructorString = "public:" ; |
103 | AppendLineBreak = true; |
104 | } else { |
105 | Loc = StructOrClass.getBraceRange().getBegin().getLocWithOffset(1); |
106 | } |
107 | } else { |
108 | Loc = AccessSpecDecl->getEndLoc().getLocWithOffset(1); |
109 | } |
110 | |
111 | DestructorString = (llvm::Twine(DestructorString) + "\nvirtual ~" + |
112 | StructOrClass.getName().str() + "() = default;" + |
113 | (AppendLineBreak ? "\n" : "" )) |
114 | .str(); |
115 | |
116 | return FixItHint::CreateInsertion(InsertionLoc: Loc, Code: DestructorString); |
117 | } |
118 | |
119 | static std::string getSourceText(const CXXDestructorDecl &Destructor) { |
120 | std::string SourceText; |
121 | llvm::raw_string_ostream DestructorStream(SourceText); |
122 | Destructor.print(DestructorStream); |
123 | return SourceText; |
124 | } |
125 | |
126 | static std::string eraseKeyword(std::string &DestructorString, |
127 | const std::string &Keyword) { |
128 | size_t KeywordIndex = DestructorString.find(str: Keyword); |
129 | if (KeywordIndex != std::string::npos) |
130 | DestructorString.erase(pos: KeywordIndex, n: Keyword.length()); |
131 | return DestructorString; |
132 | } |
133 | |
134 | static FixItHint changePrivateDestructorVisibilityTo( |
135 | const std::string &Visibility, const CXXDestructorDecl &Destructor, |
136 | const SourceManager &SM, const LangOptions &LangOpts) { |
137 | std::string DestructorString = |
138 | (llvm::Twine() + Visibility + ":\n" + |
139 | (Visibility == "public" && !Destructor.isVirtual() ? "virtual " : "" )) |
140 | .str(); |
141 | |
142 | std::string OriginalDestructor = getSourceText(Destructor); |
143 | if (Visibility == "protected" && Destructor.isVirtualAsWritten()) |
144 | OriginalDestructor = eraseKeyword(DestructorString&: OriginalDestructor, Keyword: "virtual " ); |
145 | |
146 | DestructorString = |
147 | (llvm::Twine(DestructorString) + OriginalDestructor + |
148 | (Destructor.isExplicitlyDefaulted() ? ";\n" : "" ) + "private:" ) |
149 | .str(); |
150 | |
151 | /// Semicolons ending an explicitly defaulted destructor have to be deleted. |
152 | /// Otherwise, the left-over semicolon trails the \c private: access |
153 | /// specifier. |
154 | SourceLocation EndLocation; |
155 | if (Destructor.isExplicitlyDefaulted()) |
156 | EndLocation = |
157 | utils::lexer::findNextTerminator(Start: Destructor.getEndLoc(), SM, LangOpts) |
158 | .getLocWithOffset(1); |
159 | else |
160 | EndLocation = Destructor.getEndLoc().getLocWithOffset(1); |
161 | |
162 | auto OriginalDestructorRange = |
163 | CharSourceRange::getCharRange(Destructor.getBeginLoc(), EndLocation); |
164 | return FixItHint::CreateReplacement(OriginalDestructorRange, |
165 | DestructorString); |
166 | } |
167 | |
168 | void VirtualClassDestructorCheck::check( |
169 | const MatchFinder::MatchResult &Result) { |
170 | |
171 | const auto *MatchedClassOrStruct = |
172 | Result.Nodes.getNodeAs<CXXRecordDecl>(ID: "ProblematicClassOrStruct" ); |
173 | |
174 | const CXXDestructorDecl *Destructor = MatchedClassOrStruct->getDestructor(); |
175 | if (!Destructor) |
176 | return; |
177 | |
178 | if (Destructor->getAccess() == AccessSpecifier::AS_private) { |
179 | diag(MatchedClassOrStruct->getLocation(), |
180 | "destructor of %0 is private and prevents using the type" ) |
181 | << MatchedClassOrStruct; |
182 | diag(MatchedClassOrStruct->getLocation(), |
183 | /*Description=*/"make it public and virtual" , DiagnosticIDs::Note) |
184 | << changePrivateDestructorVisibilityTo( |
185 | Visibility: "public" , Destructor: *Destructor, SM: *Result.SourceManager, LangOpts: getLangOpts()); |
186 | diag(MatchedClassOrStruct->getLocation(), |
187 | /*Description=*/"make it protected" , DiagnosticIDs::Note) |
188 | << changePrivateDestructorVisibilityTo( |
189 | Visibility: "protected" , Destructor: *Destructor, SM: *Result.SourceManager, LangOpts: getLangOpts()); |
190 | |
191 | return; |
192 | } |
193 | |
194 | // Implicit destructors are public and non-virtual for classes and structs. |
195 | bool ProtectedAndVirtual = false; |
196 | FixItHint Fix; |
197 | |
198 | if (MatchedClassOrStruct->hasUserDeclaredDestructor()) { |
199 | if (Destructor->getAccess() == AccessSpecifier::AS_public) { |
200 | Fix = FixItHint::CreateInsertion(InsertionLoc: Destructor->getLocation(), Code: "virtual " ); |
201 | } else if (Destructor->getAccess() == AccessSpecifier::AS_protected) { |
202 | ProtectedAndVirtual = true; |
203 | if (const auto MaybeRange = |
204 | getVirtualKeywordRange(Destructor: *Destructor, SM: *Result.SourceManager, |
205 | LangOpts: Result.Context->getLangOpts())) |
206 | Fix = FixItHint::CreateRemoval(RemoveRange: *MaybeRange); |
207 | } |
208 | } else { |
209 | Fix = generateUserDeclaredDestructor(StructOrClass: *MatchedClassOrStruct, |
210 | SourceManager: *Result.SourceManager); |
211 | } |
212 | |
213 | diag(MatchedClassOrStruct->getLocation(), |
214 | "destructor of %0 is %select{public and non-virtual|protected and " |
215 | "virtual}1" ) |
216 | << MatchedClassOrStruct << ProtectedAndVirtual; |
217 | diag(MatchedClassOrStruct->getLocation(), |
218 | "make it %select{public and virtual|protected and non-virtual}0" , |
219 | DiagnosticIDs::Note) |
220 | << ProtectedAndVirtual << Fix; |
221 | } |
222 | |
223 | } // namespace clang::tidy::cppcoreguidelines |
224 | |