1 //===--- VirtualClassDestructorCheck.cpp - clang-tidy -----------------===//
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
7 //===----------------------------------------------------------------------===//
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"
17 using namespace clang::ast_matchers
;
19 namespace clang::tidy::cppcoreguidelines
{
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();
30 return (((Destructor
->getAccess() == AccessSpecifier::AS_public
) &&
31 Destructor
->isVirtual()) ||
32 ((Destructor
->getAccess() == AccessSpecifier::AS_protected
) &&
33 !Destructor
->isVirtual()));
36 void VirtualClassDestructorCheck::registerMatchers(MatchFinder
*Finder
) {
37 ast_matchers::internal::Matcher
<CXXRecordDecl
> InheritsVirtualMethod
=
38 hasAnyBase(hasType(cxxRecordDecl(has(cxxMethodDecl(isVirtual())))));
42 anyOf(has(cxxMethodDecl(isVirtual())), InheritsVirtualMethod
),
44 unless(hasPublicVirtualOrProtectedNonVirtualDestructor()))
45 .bind("ProblematicClassOrStruct"),
49 static std::optional
<CharSourceRange
>
50 getVirtualKeywordRange(const CXXDestructorDecl
&Destructor
,
51 const SourceManager
&SM
, const LangOptions
&LangOpts
) {
52 if (Destructor
.getLocation().isMacroID())
55 SourceLocation VirtualBeginLoc
= Destructor
.getBeginLoc();
56 SourceLocation VirtualBeginSpellingLoc
=
57 SM
.getSpellingLoc(Destructor
.getBeginLoc());
58 SourceLocation VirtualEndLoc
= VirtualBeginSpellingLoc
.getLocWithOffset(
59 Lexer::MeasureTokenLength(VirtualBeginSpellingLoc
, SM
, LangOpts
));
61 /// Range ends with \c StartOfNextToken so that any whitespace after \c
62 /// virtual is included.
63 std::optional
<Token
> NextToken
=
64 Lexer::findNextToken(VirtualEndLoc
, SM
, LangOpts
);
67 SourceLocation StartOfNextToken
= NextToken
->getLocation();
69 return CharSourceRange::getCharRange(VirtualBeginLoc
, StartOfNextToken
);
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()};
78 AccessSpecDecl
*ASDecl
= *AS
;
79 if (ASDecl
->getAccess() == AccessSpecifier::AS_public
)
87 generateUserDeclaredDestructor(const CXXRecordDecl
&StructOrClass
,
88 const SourceManager
&SourceManager
) {
89 std::string DestructorString
;
91 bool AppendLineBreak
= false;
93 const AccessSpecDecl
*AccessSpecDecl
= getPublicASDecl(StructOrClass
);
95 if (!AccessSpecDecl
) {
96 if (StructOrClass
.isClass()) {
97 Loc
= StructOrClass
.getEndLoc();
98 DestructorString
= "public:";
99 AppendLineBreak
= true;
101 Loc
= StructOrClass
.getBraceRange().getBegin().getLocWithOffset(1);
104 Loc
= AccessSpecDecl
->getEndLoc().getLocWithOffset(1);
107 DestructorString
= (llvm::Twine(DestructorString
) + "\nvirtual ~" +
108 StructOrClass
.getName().str() + "() = default;" +
109 (AppendLineBreak
? "\n" : ""))
112 return FixItHint::CreateInsertion(Loc
, DestructorString
);
115 static std::string
getSourceText(const CXXDestructorDecl
&Destructor
) {
116 std::string SourceText
;
117 llvm::raw_string_ostream
DestructorStream(SourceText
);
118 Destructor
.print(DestructorStream
);
122 static std::string
eraseKeyword(std::string
&DestructorString
,
123 const std::string
&Keyword
) {
124 size_t KeywordIndex
= DestructorString
.find(Keyword
);
125 if (KeywordIndex
!= std::string::npos
)
126 DestructorString
.erase(KeywordIndex
, Keyword
.length());
127 return DestructorString
;
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 " : ""))
138 std::string OriginalDestructor
= getSourceText(Destructor
);
139 if (Visibility
== "protected" && Destructor
.isVirtualAsWritten())
140 OriginalDestructor
= eraseKeyword(OriginalDestructor
, "virtual ");
143 (llvm::Twine(DestructorString
) + OriginalDestructor
+
144 (Destructor
.isExplicitlyDefaulted() ? ";\n" : "") + "private:")
147 /// Semicolons ending an explicitly defaulted destructor have to be deleted.
148 /// Otherwise, the left-over semicolon trails the \c private: access
150 SourceLocation EndLocation
;
151 if (Destructor
.isExplicitlyDefaulted())
153 utils::lexer::findNextTerminator(Destructor
.getEndLoc(), SM
, LangOpts
)
154 .getLocWithOffset(1);
156 EndLocation
= Destructor
.getEndLoc().getLocWithOffset(1);
158 auto OriginalDestructorRange
=
159 CharSourceRange::getCharRange(Destructor
.getBeginLoc(), EndLocation
);
160 return FixItHint::CreateReplacement(OriginalDestructorRange
,
164 void VirtualClassDestructorCheck::check(
165 const MatchFinder::MatchResult
&Result
) {
167 const auto *MatchedClassOrStruct
=
168 Result
.Nodes
.getNodeAs
<CXXRecordDecl
>("ProblematicClassOrStruct");
170 const CXXDestructorDecl
*Destructor
= MatchedClassOrStruct
->getDestructor();
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 "public", *Destructor
, *Result
.SourceManager
, getLangOpts());
182 diag(MatchedClassOrStruct
->getLocation(),
183 /*Description=*/"make it protected", DiagnosticIDs::Note
)
184 << changePrivateDestructorVisibilityTo(
185 "protected", *Destructor
, *Result
.SourceManager
, getLangOpts());
190 // Implicit destructors are public and non-virtual for classes and structs.
191 bool ProtectedAndVirtual
= false;
194 if (MatchedClassOrStruct
->hasUserDeclaredDestructor()) {
195 if (Destructor
->getAccess() == AccessSpecifier::AS_public
) {
196 Fix
= FixItHint::CreateInsertion(Destructor
->getLocation(), "virtual ");
197 } else if (Destructor
->getAccess() == AccessSpecifier::AS_protected
) {
198 ProtectedAndVirtual
= true;
199 if (const auto MaybeRange
=
200 getVirtualKeywordRange(*Destructor
, *Result
.SourceManager
,
201 Result
.Context
->getLangOpts()))
202 Fix
= FixItHint::CreateRemoval(*MaybeRange
);
205 Fix
= generateUserDeclaredDestructor(*MatchedClassOrStruct
,
206 *Result
.SourceManager
);
209 diag(MatchedClassOrStruct
->getLocation(),
210 "destructor of %0 is %select{public and non-virtual|protected and "
212 << MatchedClassOrStruct
<< ProtectedAndVirtual
;
213 diag(MatchedClassOrStruct
->getLocation(),
214 "make it %select{public and virtual|protected and non-virtual}0",
216 << ProtectedAndVirtual
<< Fix
;
219 } // namespace clang::tidy::cppcoreguidelines