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