xref: /llvm-project/clang-tools-extra/clang-tidy/cppcoreguidelines/VirtualClassDestructorCheck.cpp (revision 11a411a49b62c129bba551df4587dd446fcdc660)
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 
AST_MATCHER(CXXRecordDecl,hasPublicVirtualOrProtectedNonVirtualDestructor)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 
registerMatchers(MatchFinder * Finder)36 void VirtualClassDestructorCheck::registerMatchers(MatchFinder *Finder) {
37   ast_matchers::internal::Matcher<CXXRecordDecl> InheritsVirtualMethod =
38       hasAnyBase(hasType(cxxRecordDecl(has(cxxMethodDecl(isVirtual())))));
39 
40   Finder->addMatcher(
41       cxxRecordDecl(
42           anyOf(has(cxxMethodDecl(isVirtual())), InheritsVirtualMethod),
43           unless(isFinal()),
44           unless(hasPublicVirtualOrProtectedNonVirtualDestructor()))
45           .bind("ProblematicClassOrStruct"),
46       this);
47 }
48 
49 static std::optional<CharSourceRange>
getVirtualKeywordRange(const CXXDestructorDecl & Destructor,const SourceManager & SM,const LangOptions & LangOpts)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(Destructor.getBeginLoc());
58   SourceLocation VirtualEndLoc = VirtualBeginSpellingLoc.getLocWithOffset(
59       Lexer::MeasureTokenLength(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(VirtualEndLoc, SM, LangOpts);
65   if (!NextToken)
66     return std::nullopt;
67   SourceLocation StartOfNextToken = NextToken->getLocation();
68 
69   return CharSourceRange::getCharRange(VirtualBeginLoc, StartOfNextToken);
70 }
71 
72 static const AccessSpecDecl *
getPublicASDecl(const CXXRecordDecl & StructOrClass)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
generateUserDeclaredDestructor(const CXXRecordDecl & StructOrClass,const SourceManager & SourceManager)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(Loc, DestructorString);
113 }
114 
getSourceText(const CXXDestructorDecl & Destructor)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 
eraseKeyword(std::string & DestructorString,const std::string & Keyword)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;
128 }
129 
changePrivateDestructorVisibilityTo(const std::string & Visibility,const CXXDestructorDecl & Destructor,const SourceManager & SM,const LangOptions & LangOpts)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(OriginalDestructor, "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(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 
check(const MatchFinder::MatchResult & Result)164 void VirtualClassDestructorCheck::check(
165     const MatchFinder::MatchResult &Result) {
166 
167   const auto *MatchedClassOrStruct =
168       Result.Nodes.getNodeAs<CXXRecordDecl>("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                "public", *Destructor, *Result.SourceManager, getLangOpts());
182     diag(MatchedClassOrStruct->getLocation(),
183          /*Description=*/"make it protected", DiagnosticIDs::Note)
184         << changePrivateDestructorVisibilityTo(
185                "protected", *Destructor, *Result.SourceManager, 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(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);
203     }
204   } else {
205     Fix = generateUserDeclaredDestructor(*MatchedClassOrStruct,
206                                          *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